Catalog · How the catalog works
vmRedisClient4D
Client Redis per Delphi
- ★ 2 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
vmRedisClient
Native Redis client for Delphi with full support for the RESP protocol, connection pooling, Pipeline, Pub/Sub, Lua scripting and SSL/TLS. Built from scratch with no external dependencies, it provides a clean and intuitive interface for all Redis operations.
English | Italiano
📋 Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Transport Layer
- Command Reference
- Pipeline
- Lua Scripting
- Pub/Sub
- Health Check
- SSL/TLS
- Connection Pool
- Practical Examples
- Architecture
- Error Handling
- Advanced Configuration
- Best Practices
- Roadmap
- Contributing
- License
✨ Features
Core Features
- ✅ Full RESP protocol — Native Redis Serialization Protocol support
- ✅ All Redis data types — String, List, Set, Hash, Sorted Set, HyperLogLog
- ✅ Connection Pooling — Thread-safe pool with statistics, idle cleanup and automatic health check
- ✅ Pipeline — Batch commands in a single round-trip, multiple responses
- ✅ Pub/Sub — Dedicated Publisher and Subscriber with pattern matching and non-blocking listener
- ✅ Transactions — MULTI/EXEC/DISCARD with WATCH/UNWATCH for optimistic locking
- ✅ Lua Scripting — EVAL, EVALSHA, SCRIPT LOAD
- ✅ SSL/TLS — Secure connections with certificate validation and mTLS
- ✅ Health Check — Detailed monitoring with timestamp, ping time and transport status
- ✅ Logging — Extensible system via
IRedisLogger(Command/Response/Error/Warning/Info/Debug) - ✅ Generic command —
ExecuteCommandas an escape hatch for any Redis command - ✅ Pluggable Transport Layer — Unified
IRedisTransport, Registry with auto-registration, Synapse and GrijjyFoundation support
Supported Commands by Type
- Strings — SET, GET, MGET, MSET, MSETNX, GETSET, SETEX, PSETEX, SETNX, GETEX, GETEXPERSIST, GETDEL, APPEND, STRLEN, SETRANGE, GETRANGE, INCR, INCRBY, INCRBYFLOAT, DECR, DECRBY
- Keys — DEL, UNLINK, EXISTS, EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT, TTL, PTTL, EXPIRETIME, PEXPIRETIME (Redis 7+), PERSIST, TYPE, RENAME, RENAMENX, COPY, RANDOMKEY, SCAN, KEYS
- Lists — LPUSH, RPUSH, LPUSHX, RPUSHX, LPOP, RPOP, LRANGE, LINDEX, LSET, LINSERT, LREM, LTRIM, LMOVE, LLEN
- Sets — SADD, SREM, SMEMBERS, SISMEMBER, SMISMEMBER, SCARD, SPOP, SRANDMEMBER, SMOVE, SUNION/STORE, SINTER/STORE, SDIFF/STORE, SSCAN
- Hashes — HSET, HGET, HGETALL, HMGET, HMSET, HDEL, HEXISTS, HKEYS, HVALS, HLEN, HINCRBY, HINCRBYFLOAT, HRANDFIELD, HSCAN
- Sorted Sets — ZADD (NX/XX/GT/LT/CH via ZAddOpt), ZREM, ZSCORE, ZINCRBY, ZCARD, ZCOUNT, ZRANK, ZREVRANK, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZPOPMIN, ZPOPMAX, ZRANDMEMBER, ZSCAN
- HyperLogLog — PFADD, PFCOUNT, PFMERGE
- Server/Admin — DBSIZE, FLUSHDB, INFO, SELECT, CLIENT SETNAME/GETNAME, CONFIG GET/SET
Redis Support
- Redis 6.x, 7.x, 8.x
- ACL authentication (username + password) and legacy (password only)
- Database selection with automatic pool management (SELECT fix)
EXPIRETIME/PEXPIRETIME(Redis 7+)
Compatibility
- Delphi 10.4 Sydney and above — Windows 32-bit and 64-bit, Linux, macOS (with Grijjy)
- Managed records with Initialize/Finalize/Assign operators
- Interface-based design for automatic memory management
- Amazon ElastiCache and Azure Cache for Redis
- RESP2 protocol (RESP3 ready for future extension)
📦 Requirements
- Delphi 10.4 Sydney or above
- Redis Server 6.x, 7.x or 8.x
- Synapse — bundled with the project (transport
synapse, default) - GrijjyFoundation — bundled with the project (transport
grijjy, cross-platform) - OpenSSL 3.x DLLs (
libssl-3-x64.dll,libcrypto-3-x64.dll) — only for SSL/TLS with Synapse
🚀 Installation
git clone https://github.com/MuzioValerio/vmRedisClient4D.gitAdd to your Delphi project's Library Path:
vmRedisClient4D\sources
vmRedisClient4D\synapse
vmRedisClient4D\GrijjyFoundation🎯 Quick Start
Simple Connection (Synapse transport — default)
uses
Redis.Client, Redis.Connection.Config, Redis.Types, Redis.Response,
Redis.Transport.Synapse; // auto-registers the 'synapse' provider
var
lConfig: TRedisConnectionConfig; // managed record: auto-initialized
lClient: IRedisClient;
begin
lConfig := TRedisConnectionConfig.Localhost; // host=localhost, port=6379
lClient := TRedisClient.New(lConfig); // 'synapse' provider by default
lClient.Connect;
try
if lClient.Ping then
WriteLn('Connected to Redis!');
lClient.SetValue('mykey', 'Hello Redis!');
WriteLn(lClient.Get('mykey').AsString);
finally
lClient.Disconnect;
end;
end;Connection with GrijjyFoundation transport (cross-platform)
uses
Redis.Client, Redis.Connection.Config,
Redis.Transport.Grijjy; // auto-registers the 'grijjy' provider
var
lClient: IRedisClient;
begin
lClient := TRedisClient.New(
TRedisConnectionConfig.Localhost,
nil, // optional logger
'grijjy' // explicit provider
);
lClient.Connect;
// ... usage identical to Synapse
end;Authentication
// Redis 6+ with ACL
lConfig := TRedisConnectionConfig.WithACL('localhost', 6379, 'myuser', 'mypassword', 0);
// Redis < 6 — password only
lConfig := TRedisConnectionConfig.WithAuth('localhost', 6379, 'mypassword', 0);With Logger
uses Redis.HealthCheck; // TSimpleRedisLogger
lLogger := TSimpleRedisLogger.New;
lClient := TRedisClient.New(lConfig, lLogger);🔌 Transport Layer
Overview
Since v2.1 the transport layer is fully decoupled from the Redis client. All implementations share a unified interface (IRedisTransport) and self-register in a global registry (TRedisTransportRegistry). The client has no knowledge of or dependency on the concrete transport.
IRedisDataReader ??? raw data reading (used by TRedisProtocolParser)
????????? IRedisTransport ??? unified interface for all transports
????????? TRedisNativeTransport (Synapse, provider 'synapse')
????????? TRedisGrijjyTransport (GrijjyFoundation, provider 'grijjy')IRedisSynapseTransport is kept as a compatibility alias — existing code using it requires no changes.
IRedisTransport — Unified Interface
Defined in Redis.Transport.Intf, it replaces the former IRedisSynapseTransport (now an alias) and the old minimal IRedisTransport.
// Redis.Transport.Intf.pas
// Raw data reading — used by TRedisProtocolParser
IRedisDataReader = interface
function ReadLine: string;
function ReadBytes(aCount: Integer): TBytes;
end;
// Unified interface for all Redis transports
IRedisTransport = interface(IRedisDataReader)
procedure Connect;
procedure Disconnect;
function IsConnected: Boolean;
// Raw data send
procedure Send(const aData: TBytes);
// Serialize and send a RESP command
procedure SendCommand(const aParts: array of string);
// Pipeline: buffer commands without sending them
procedure PipelineCommand(const aParts: array of string);
// Flush the entire pipeline buffer in a single write
function FlushPipeline: TBytes;
function IsPipelining: Boolean;
end;
// Backward-compatibility alias
IRedisSynapseTransport = IRedisTransport;Transport Registry
TRedisTransportRegistry (in Redis.Transport.Registry) is the central point for registering and acquiring transports.
// Register a new provider (called in the unit's initialization section)
TRedisTransportRegistry.RegisterProvider('myprovider',
function(const aConfig: TRedisConnectionConfig; const aLogger: IRedisLogger): IRedisTransport
begin
Result := TMyTransport.New(aConfig, aLogger);
end);
// Acquire a transport by ID
var lTransport := TRedisTransportRegistry.Acquire('synapse', lConfig, lLogger);
var lTransport := TRedisTransportRegistry.Acquire('grijjy', lConfig, lLogger);ID lookup is case-insensitive. If the provider is not registered, an exception is raised with an explicit message.
Important: to activate a provider simply add its unit to the project. The
initializationsection of the unit handles auto-registration.
Synapse Provider (default)
Unit: Redis.Transport.Synapse
Provider ID: 'synapse'
Platforms: Windows 32/64-bit
The original transport based on Synapse. It is the default when calling TRedisClient.New without specifying a provider.
uses Redis.Transport.Synapse; // auto-registration on unit load
lClient := TRedisClient.New(lConfig); // implicit 'synapse'
lClient := TRedisClient.New(lConfig, nil, 'synapse'); // explicitFeatures:
- SSL/TLS via OpenSSL (requires
libssl-3-x64.dll/libcrypto-3-x64.dll) - mTLS with client certificate
- Automatic retry on connect and authentication
Grijjy Provider (cross-platform)
Unit: Redis.Transport.Grijjy
Provider ID: 'grijjy'
Platforms: Windows, Linux, macOS, iOS, Android
High-performance transport based on GrijjyFoundation. Uses event-based asynchronous I/O (TgoClientSocketManager) and native SSL/TLS with no additional OpenSSL dependencies.
uses Redis.Transport.Grijjy; // auto-registration on unit load
lClient := TRedisClient.New(lConfig, nil, 'grijjy');
lClient.Connect;Features:
- Native cross-platform SSL/TLS (GrijjyFoundation / BoringSSL)
- mTLS with client certificate (
FSocket.Certificate,FSocket.Password) - Non-blocking async I/O — synchronized via
TEventfor compatibility with the synchronous protocol - Thread-safe receive buffer protected by
TCriticalSection - Auto-registration in the registry with ID
'grijjy'
Compile-time dependencies:
{$IFDEF MSWINDOWS}
Grijjy.SocketPool.Win
{$ELSE}
Grijjy.SocketPool.Linux
{$ENDIF}
Grijjy.OpenSSLSSL with Grijjy:
lConfig := TRedisConnectionConfig.WithSSL(
'redis.example.com', 6380, 'user', 'pass', True
);
// mTLS — client certificate
lConfig.ClientCertificatePath := '/etc/certs/client.pem';
lConfig.ClientCertificatePassword := 'certpass';
lClient := TRedisClient.New(lConfig, nil, 'grijjy');
lClient.Connect;Choosing a Transport
| Criterion | Synapse | Grijjy |
|---|---|---|
| Platforms | Windows | Windows, Linux, macOS, iOS, Android |
| SSL/TLS | OpenSSL (external DLL) | Native (BoringSSL bundled) |
| I/O model | Synchronous blocking | Asynchronous event-based |
| Performance | High | Very high |
| Dependencies | Synapse + OpenSSL DLL | GrijjyFoundation |
| Default | ✅ Yes | No (include the unit) |
Implementing a Custom Transport
To create a new transport, implement IRedisTransport and register it:
unit Redis.Transport.MyCustom;
interface
uses
Redis.Transport.Intf, Redis.Connection.Config, Redis.Types;
type
TMyCustomTransport = class(TInterfacedObject, IRedisTransport)
private
// ... private fields ...
public
{ IRedisTransport }
procedure Connect;
procedure Disconnect;
function IsConnected: Boolean;
procedure Send(const aData: TBytes);
procedure SendCommand(const aParts: array of string);
procedure PipelineCommand(const aParts: array of string);
function FlushPipeline: TBytes;
function IsPipelining: Boolean;
{ IRedisDataReader }
function ReadLine: string;
function ReadBytes(aCount: Integer): TBytes;
class function New(const aConfig: TRedisConnectionConfig;
aLogger: IRedisLogger): IRedisTransport;
end;
implementation
uses Redis.Transport.Registry;
// ... implementation ...
initialization
TRedisTransportRegistry.RegisterProvider('mycustom',
function(const aConfig: TRedisConnectionConfig; const aLogger: IRedisLogger): IRedisTransport
begin
Result := TMyCustomTransport.New(aConfig, aLogger);
end);
end.Then simply add the unit to your project and use TRedisClient.New(lConfig, nil, 'mycustom').
📚 Command Reference
Keys and TTL
lClient.Del(['key1', 'key2']);
lClient.Unlink(['key1', 'key2']); // non-blocking
lCount := lClient.Exists(['key1', 'key2']);
lClient.Expire('key', 300);
lClient.PExpire('key', 300000); // milliseconds
lClient.ExpireAt('key', UnixTimestamp);
lClient.PExpireAt('key', UnixTimestampMs);
lClient.Persist('key'); // remove expiry
lTTL := lClient.TTL('key'); // -1=no expiry, -2=does not exist
lPTTL := lClient.PTTL('key');
lET := lClient.ExpireTime('key'); // Redis 7+
lPET := lClient.PExpireTime('key'); // Redis 7+
lType := lClient.KeyType('key'); // "string","list","set","zset","hash"
lClient.Rename('old', 'new');
lClient.RenameNX('old', 'new');
lRnd := lClient.RandomKey;
lClient.Copy('src', 'dst', True); // True = overwrite dst
// TRedisScanResult.Cursor = 0 when iteration is complete
lScan := lClient.Scan(0, 'user:*', 100);Strings
lClient.SetValue('key', 'value');
lClient.SetEx('key', 'value', 3600);
lClient.PSetEx('key', 'value', 3600000);
lClient.SetNX('key', 'value');
lResponse := lClient.Get('key');
lResponse := lClient.GetEx('key', 60); // GET + update TTL
lResponse := lClient.GetExPersist('key'); // GET + remove TTL
lResponse := lClient.GetDel('key'); // atomic GET + DELETE
lResponse := lClient.GetSet('key', 'new'); // atomic GET + SET
lValues := lClient.MGet(['key1', 'key2', 'key3']);
lClient.MSet(['k1', 'v1', 'k2', 'v2']);
lClient.MSetNX(['k1', 'v1', 'k2', 'v2']);
lVal := lClient.Incr('counter');
lVal := lClient.IncrBy('counter', 10);
lDbl := lClient.IncrByFloat('price', 1.5);
lVal := lClient.Decr('counter');
lVal := lClient.DecrBy('counter', 5);
lLen := lClient.Append('key', ' suffix');
lLen := lClient.StrLen('key');
lLen := lClient.SetRange('key', 6, 'World');
lSub := lClient.GetRange('key', 0, 4);Lists
lClient.LPush('list', ['a', 'b', 'c']);
lClient.RPush('list', ['d', 'e']);
lClient.LPushX('list', ['x']);
lClient.RPushX('list', ['y']);
lVal := lClient.LPop('list');
lVal := lClient.RPop('list');
lLen := lClient.LLen('list');
lItems := lClient.LRange('list', 0, -1);
lVal := lClient.LIndex('list', 0);
lClient.LSet('list', 0, 'newval');
lClient.LInsert('list', True, 'pivot', 'before_pivot'); // True=BEFORE
lClient.LInsert('list', False, 'pivot', 'after_pivot'); // False=AFTER
lClient.LRem('list', 1, 'value');
lClient.LTrim('list', 0, 9);
lVal := lClient.LMove('src', 'dst', True, False); // LPOP src → RPUSH dstSets
lClient.SAdd('set', ['a', 'b', 'c']);
lClient.SRem('set', ['a']);
lMembers := lClient.SMembers('set');
lBool := lClient.SIsMember('set', 'b');
lBools := lClient.SMIsMember('set', ['b', 'c', 'x']);
lCount := lClient.SCard('set');
lPop := lClient.SPop('set', 2);
lRand := lClient.SRandMember('set', 3);
lClient.SMove('src', 'dst', 'member');
lItems := lClient.SUnion(['set1', 'set2']);
lClient.SUnionStore('dst', ['set1', 'set2']);
lItems := lClient.SInter(['set1', 'set2']);
lClient.SInterStore('dst', ['set1', 'set2']);
lItems := lClient.SDiff(['set1', 'set2']);
lClient.SDiffStore('dst', ['set1', 'set2']);
lScan := lClient.SScan('set', 0, 'a*', 100);Hashes
lClient.HSet('hash', 'field', 'value');
lVal := lClient.HGet('hash', 'field');
lAll := lClient.HGetAll('hash');
lVals := lClient.HMGet('hash', ['f1', 'f2']);
lClient.HMSet('hash', ['f1', 'v1', 'f2', 'v2']);
lClient.HDel('hash', ['f1', 'f2']);
lBool := lClient.HExists('hash', 'field');
lKeys := lClient.HKeys('hash');
lVals := lClient.HVals('hash');
lLen := lClient.HLen('hash');
lVal := lClient.HIncrBy('hash', 'counter', 5);
lDbl := lClient.HIncrByFloat('hash', 'price', 1.5);
lRand := lClient.HRandField('hash', 3);
lScan := lClient.HScan('hash', 0, 'field*', 100);Sorted Sets
lClient.ZAdd('zset', 100.5, 'player1');
// ZAddOpt: options rzaNX/rzaXX/rzaGT/rzaLT, aCH=True counts changed elements
lCount := lClient.ZAddOpt('zset', 200.0, 'player1', rzaGT, True);
lClient.ZRem('zset', ['player1']);
lScore := lClient.ZScore('zset', 'player2');
lDbl := lClient.ZIncrBy('zset', 10.0, 'player2');
lCount := lClient.ZCard('zset');
lCount := lClient.ZCount('zset', '50', '+inf');
lRank := lClient.ZRank('zset', 'player2');
lRank := lClient.ZRevRank('zset', 'player2');
lItems := lClient.ZRange('zset', 0, 9);
lItems := lClient.ZRange('zset', 0, 9, True);
lItems := lClient.ZRevRange('zset', 0, 9, True);
lItems := lClient.ZRangeByScore('zset', '50', '+inf', True, 0, 10);
lItems := lClient.ZRevRangeByScore('zset', '+inf', '50', True);
lItems := lClient.ZPopMin('zset', 3);
lItems := lClient.ZPopMax('zset', 3);
lItems := lClient.ZRandMember('zset', 5);
lScan := lClient.ZScan('zset', 0, 'player*', 100);HyperLogLog
// High-cardinality unique counting — ~0.81% estimated error, constant memory
lClient.PFAdd('visitors:today', ['user1', 'user2', 'user3']);
lCount := lClient.PFCount(['visitors:today']);
lClient.PFMerge('visitors:total', ['visitors:mon', 'visitors:tue']);Transactions with Optimistic Locking
lClient.Watch(['account:1', 'account:2']);
try
lClient.Multi;
try
lClient.IncrBy('account:1', -100);
lClient.IncrBy('account:2', 100);
lResult := lClient.Exec;
if lResult.IsNull then
WriteLn('Aborted: key modified by another client')
else
WriteLn('Transfer completed');
except
lClient.Discard;
raise;
end;
finally
lClient.Unwatch;
end;Server/Admin Commands
lSize := lClient.DbSize;
lInfo := lClient.Info('server');
lClient.FlushDb(False); // False=synchronous, True=asynchronous
lClient.Select(2); // ⚠️ do not use on pooled connections
lClient.ClientSetName('my-app');
lName := lClient.ClientGetName;
lValues := lClient.ConfigGet('maxmemory');
lClient.ConfigSet('hz', '15');Generic Command
// Escape hatch for commands not yet directly implemented
lResponse := lClient.ExecuteCommand(['OBJECT', 'ENCODING', 'mykey']);
lResponse := lClient.ExecuteCommand(['XLEN', 'mystream']);⚡ Pipeline
Pipeline mode sends all commands in a single round-trip and collects all responses in one read, dramatically reducing latency for batch operations.
lClient.BeginPipeline;
lClient.PipelineCommand(['SET', 'key1', 'value1']);
lClient.PipelineCommand(['SET', 'key2', 'value2']);
lClient.PipelineCommand(['INCR', 'counter']);
lClient.PipelineCommand(['GET', 'key1']);
lResponses := lClient.ExecPipeline;
for var I := 0 to High(lResponses) do
WriteLn(Format('[%d] %s', [I, lResponses[I].AsString]));Bulk import — 1000 SETs in a single round-trip:
lClient.BeginPipeline;
for var I := 1 to 1000 do
lClient.PipelineCommand(['SET',
Format('product:%d', [I]),
Format('{"id":%d,"name":"Item %d"}', [I, I])
]);
lResponses := lClient.ExecPipeline;
WriteLn(Format('Imported %d products', [Length(lResponses)]));🌙 Lua Scripting
// Load and get SHA1 (run once only)
lSha := lClient.ScriptLoad(
'return redis.call("SET", KEYS[1], ARGV[1])'
);
// Execute by SHA (script already cached on server)
lResult := lClient.EvalSha(lSha, ['mykey'], ['myvalue']);
// Direct execution
lResult := lClient.Eval(
'return redis.call("GET", KEYS[1])',
['mykey'], []
);
WriteLn(lResult.AsString);📡 Pub/Sub
Publish
// Directly from the main connection
lCount := lClient.Publish('events', 'user.login');
// Dedicated publisher — with channel info
lPublisher := lClient.CreatePublisher;
lPublisher.Connect;
try
lCount := lPublisher.Publish('news', 'Breaking news!');
lChannels := lPublisher.PubSubChannels('news:*');
lSubMap := lPublisher.PubSubNumSub(['news', 'alerts']);
lPatterns := lPublisher.PubSubNumPat;
finally
lPublisher.Disconnect;
end;Blocking Subscriber
lSubscriber := lClient.CreateSubscriber;
lSubscriber.Connect;
lSubscriber.Subscribe(['news', 'alerts'],
procedure(const aMsg: TRedisPubSubMessage)
begin
case aMsg.MessageType of
rmtMessage:
WriteLn(Format('[%s] %s', [aMsg.Channel, aMsg.Payload]));
rmtSubscribe:
WriteLn(Format('Subscribed to %s. Total: %d',
[aMsg.Channel, aMsg.SubscriptionCount]));
end;
end
);
lSubscriber.StartListening; // blockingNon-Blocking Subscriber (dedicated thread)
lSubscriber.PSubscribe(['news:*'],
procedure(const aMsg: TRedisPubSubMessage)
begin
case aMsg.MessageType of
rmtPMessage:
WriteLn(Format('[%s→%s] %s',
[aMsg.Pattern, aMsg.Channel, aMsg.Payload]));
end;
end
);
lThread := TRedisPubSubListenerThread.Create(lSubscriber);
lThread.Start;
try
ReadLn; // application keeps running
finally
lSubscriber.StopListening;
lThread.WaitFor;
lThread.Free;
lSubscriber.Disconnect;
end;Full Pattern: Event Manager
type
TRedisEventManager = class
private
FMainClient: IRedisClient;
FPublisher: IRedisPubSubPublisher;
FSubscriber: IRedisPubSubClient;
FThread: TRedisPubSubListenerThread;
public
constructor Create(const aConfig: TRedisConnectionConfig;
aLogger: IRedisLogger = nil);
destructor Destroy; override;
procedure PublishEvent(const aType, aData: string);
procedure StartListening(aCallback: TRedisMessageCallback);
procedure StopListening;
end;
constructor TRedisEventManager.Create(const aConfig: TRedisConnectionConfig;
aLogger: IRedisLogger);
begin
inherited Create;
FMainClient := TRedisClient.New(aConfig, aLogger);
FMainClient.Connect;
FPublisher := FMainClient.CreatePublisher;
FPublisher.Connect;
FSubscriber := FMainClient.CreateSubscriber;
FSubscriber.Connect;
end;
destructor TRedisEventManager.Destroy;
begin
StopListening;
FPublisher.Disconnect;
FSubscriber.Disconnect;
FMainClient.Disconnect;
inherited;
end;
procedure TRedisEventManager.StartListening(aCallback: TRedisMessageCallback);
begin
FSubscriber.Subscribe(
['user.login', 'user.logout', 'system.error'], aCallback);
FThread := TRedisPubSubListenerThread.Create(FSubscriber);
FThread.Start;
end;
procedure TRedisEventManager.StopListening;
begin
if Assigned(FSubscriber) and FSubscriber.IsListening then
begin
FSubscriber.StopListening;
if Assigned(FThread) then
begin
FThread.WaitFor;
FreeAndNil(FThread);
end;
end;
end;🩺 Health Check
lHealth := lClient.HealthCheckDetailed;
// ToString produces:
// Redis Health Status [12/03/2026 14:30:00]
// Healthy: True
// Transport: True
// PING: True (3 ms)
// Commands: True
// Message:
WriteLn(lHealth.ToString);
if not lHealth.IsHealthy then
WriteLn('⚠️ ' + lHealth.ErrorMessage);
lClient.LogHealthStatus; // logs directly to the configured loggerContinuous monitoring:
var lFailCount := 0;
const MAX_FAILS = 3;
begin
while True do
begin
lHealth := lClient.HealthCheckDetailed;
if lHealth.IsHealthy then
begin
WriteLn(Format('✓ Ping: %d ms', [lHealth.PingResponseTimeMs]));
lFailCount := 0;
end
else
begin
Inc(lFailCount);
WriteLn(Format('✗ [%d/%d] %s', [lFailCount, MAX_FAILS, lHealth.ErrorMessage]));
if lFailCount >= MAX_FAILS then
begin
lClient.Disconnect;
Sleep(1000);
lClient.Connect;
lFailCount := 0;
end;
end;
Sleep(5000);
end;
end;🔐 SSL/TLS
lConfig := TRedisConnectionConfig.WithSSL(
'redis.example.com', 6380, 'username', 'password', True
);
// mTLS — client certificate authentication
lConfig.ClientCertificatePath := 'C:\certs\client.pem';
lConfig.ClientCertificatePassword := 'certpass';
// With Synapse (Windows, OpenSSL DLL)
lClient := TRedisClient.New(lConfig, nil, 'synapse');
// With Grijjy (cross-platform, native SSL)
lClient := TRedisClient.New(lConfig, nil, 'grijjy');
lClient.Connect;🔗 Connection Pool
What is Connection Pooling?
Connection Pooling maintains a set of already-open Redis connections ready for use, eliminating the overhead of creating and destroying a TCP connection for every single operation.
| Aspect | Without Pool | With Pool |
|---|---|---|
| Latency | High (TCP handshake + auth per op.) | Low (connection ready) |
| Throughput | Limited | High (connections reused) |
| Resources | Inefficient | Optimized |
| Scalability | Problematic | Excellent |
Internal Architecture
The pool is managed by TRedisConnectionPool (implements IRedisConnectionPool). Internally it uses a TObjectList<TPooledConnection> protected by a TCriticalSection. Each TPooledConnection wraps an IRedisClient and tracks:
- IsHealthy — checks that the connection responds correctly
- IdleTimeMs — how long it has been idle
- ErrorCount — consecutive errors (threshold 3 → automatic removal)
- CurrentDb — currently selected database (for automatic
SELECT)
Configuration
TRedisPoolConfig is a managed record that auto-initializes with sensible defaults:
uses Redis.Pool;
var
lPoolConfig: TRedisPoolConfig;
// Automatic defaults:
// MinConnections = 2
// MaxConnections = 10
// AcquireTimeout = 5000 ms
// IdleTimeout = 300000 ms (5 minutes)
// ValidationInterval = 30000 ms
// EnableHealthCheck = True
lPool: IRedisConnectionPool;
begin
lPoolConfig.MinConnections := 2;
lPoolConfig.MaxConnections := 10;
lPoolConfig.AcquireTimeout := 5000;
lPool := TRedisConnectionPool.New(lPoolConfig, lRedisConfig);
lPool.Initialize;
try
// ... use the pool
finally
lPool.Shutdown;
end;
end;Pattern 1 — Helper (recommended)
TRedisPoolHelper automatically handles acquire, execution, and release — even on exception.
// Procedure
TRedisPoolHelper.Execute(lPool,
procedure(aClient: IRedisClient)
begin
aClient.SetValue('last_access', DateTimeToStr(Now));
aClient.Incr('request_count');
end
);
// Typed return value
var lName := TRedisPoolHelper.Execute<string>(lPool,
function(aClient: IRedisClient): string
begin
Result := aClient.Get('user:1:name').AsString;
end
);Pattern 2 — Manual Acquire/Release
var lClient := lPool.AcquireConnection;
try
lClient.SetValue('key', 'value');
WriteLn(lClient.Get('key').AsString);
finally
lPool.ReleaseConnection(lClient); // ALWAYS in finally
end;Pattern 3 — Managed Record (auto-release)
TPooledRedisClient is a managed record: its automatic Finalize calls Release at the end of the scope.
procedure DoSomething;
var
lPooled: TPooledRedisClient;
begin
lPooled := TPooledRedisClient.Create(lPool);
lPooled.Client.Incr('counter');
lPooled.Client.SetValue('status', 'active');
end; // ← Finalize automatically calls ReleasePattern 4 — Specific Database
The pool automatically issues SELECT if the acquired connection is not already on the requested database.
var lClient := lPool.AcquireConnectionOnDb(3);
try
lClient.SetValue('session:abc', 'data');
finally
lPool.ReleaseConnection(lClient);
end;⚠️ Do not call
lClient.Select()manually on pooled connections: useAcquireConnectionOnDbinstead.
Statistics and Monitoring
var lStats := lPool.GetStats;
WriteLn(lStats.ToString);
// Output:
// Redis Pool Statistics
// Total Connections: 5
// Active: 2 | Idle: 3 | Waiting: 0
// Lifetime Stats:
// Acquired: 1234 | Released: 1234
// Created: 5 | Destroyed: 0
// Errors: 0
// Avg Acquire Time: 0.35 ms
lPool.LogStats;Metrics to watch:
- WaitingRequests > 0 → pool saturated, increase
MaxConnections - AverageAcquireTimeMs rising → connection contention
- TotalErrors → non-healthy connections automatically removed
Concurrent Operations
var lTasks: array[1..5] of ITask;
for var I := 1 to 5 do
begin
var lNum := I;
lTasks[I] := TTask.Run(
procedure
begin
for var J := 1 to 10 do
TRedisPoolHelper.Execute(lPool,
procedure(aClient: IRedisClient)
begin
aClient.Incr(Format('thread:%d:counter', [lNum]));
Sleep(Random(100));
end
);
end
);
end;
TTask.WaitForAll(lTasks);
lPool.LogStats;Load Testing
// Built-in helper for multi-thread stress tests
TRedisThreadHelper.WorkerThread(lPool, ThreadNum, Iterations);Production Checklist
- ✅ Call
Pool.Initializeat application startup - ✅ Call
Pool.Shutdownat shutdown to free all resources - ✅ Size
MaxConnectionsbased on the number of concurrent threads/requests - ✅ Use
IRedisLoggerto trace automatic removal of non-healthy connections - ✅ Monitor
WaitingRequestsandAverageAcquireTimeMsin production - ✅ Use
AcquireConnectionOnDbinstead of manualSelect
💡 Practical Examples
Cache Application
function GetUserData(aUserId: Integer): string;
var
lClient: IRedisClient;
lKey: string;
lCached: IRedisResponse;
begin
lKey := Format('user:%d', [aUserId]);
lClient := TRedisClient.New(FConfig);
lClient.Connect;
try
lCached := lClient.Get(lKey);
if not lCached.IsNull then
Exit(lCached.AsString); // cache hit
Result := LoadUserFromDatabase(aUserId);
lClient.SetEx(lKey, Result, 3600); // cache for 1 hour
finally
lClient.Disconnect;
end;
end;Rate Limiting
function CheckRateLimit(const aUserId, aAction: string): Boolean;
const
MAX_REQUESTS = 100;
WINDOW_SECONDS = 3600;
var
lClient: IRedisClient;
lKey: string;
lCount: Int64;
begin
lKey := Format('ratelimit:%s:%s', [aUserId, aAction]);
lClient := TRedisClient.New(FConfig);
lClient.Connect;
try
lCount := lClient.Incr(lKey);
if lCount = 1 then
lClient.Expire(lKey, WINDOW_SECONDS);
Result := lCount <= MAX_REQUESTS;
finally
lClient.Disconnect;
end;
end;Distributed Lock
function AcquireLock(const aResource: string; aTimeoutSec: Integer): string;
var
lClient: IRedisClient;
lToken: string;
begin
lToken := TGuid.NewGuid.ToString;
lClient := TRedisClient.New(FConfig);
lClient.Connect;
try
if lClient.SetNX('lock:' + aResource, lToken) then
begin
lClient.Expire('lock:' + aResource, aTimeoutSec);
Result := lToken; // token for safe release
end
else
Result := '';
finally
lClient.Disconnect;
end;
end;Leaderboard
const BOARD = 'game:leaderboard';
// Update only if the score improves
lClient.ZAddOpt(BOARD, aScore, aPlayer, rzaGT);
// Top 10 with scores (descending order)
lTop := lClient.ZRevRange(BOARD, 0, 9, True);
// ['player1','1500.0','player2','1200.0',...]
// Player rank (0-based, from highest)
lRank := lClient.ZRevRank(BOARD, aPlayer);🏗️ Architecture
Project Structure
vmRedisClient4D/
????????? sources/
??? ????????? Redis.Client.Intf.pas # IRedisClient interface
??? ????????? Redis.Client.pas # TRedisClient implementation
??? ????????? Redis.Connection.Config.pas # TRedisConnectionConfig (managed record)
??? ????????? Redis.Protocol.pas # RESP parser + serializer
??? ????????? Redis.Response.pas # IRedisResponse / TRedisResponse
??? ????????? Redis.Transport.Intf.pas # IRedisTransport / IRedisDataReader (NEW v2.1)
??? ????????? Redis.Transport.Registry.pas # TRedisTransportRegistry (NEW v2.1)
??? ????????? Redis.Transport.Synapse.pas # Synapse transport (Windows)
??? ????????? Redis.Transport.Grijjy.pas # GrijjyFoundation transport (cross-platform, NEW v2.1)
??? ????????? Redis.Logger.Empty.pas # TRedisEmptyLogger (no-op logger)
??? ????????? Redis.Types.pas # Types, exceptions, TRedisScanResult
??? ????????? Redis.Pool.pas # Pool + helper + statistics
??? ????????? Redis.PubSub.pas # Publisher, Subscriber, ListenerThread
??? ????????? Redis.HealthCheck.pas # TSimpleRedisLogger
????????? synapse/
????????? GrijjyFoundation/
????????? example/
??? ????????? ExampleConsole.dpr
??? ????????? ExamplePooling.dpr # 8 pooling and concurrency patterns
??? ????????? ExamplePubSub.dpr # 3 Pub/Sub patterns + Event Manager
??? ????????? ExampleHealthCheck.dpr
????????? unit_test/
????????? Redis.Client.Tests.pas
????????? Redis.Pool.Tests.pas
????????? Redis.PubSub.Tests.pasDesign Patterns
- Interface-Based Design — Automatic memory management, zero memory leaks
- Factory Pattern —
TRedisClient.New,TRedisConnectionConfig.Localhost/WithACL/WithSSL,TRedisConnectionPool.New - Observer Pattern — Pub/Sub with
TRedisMessageCallback - Object Pool Pattern —
TRedisConnectionPool+TPooledConnection+ statistics - Strategy Pattern — Replaceable logger via
IRedisLogger; swappable transport viaIRedisTransport+ Registry - Registry Pattern —
TRedisTransportRegistrywith auto-registration viainitialization - Managed Record —
TRedisConnectionConfig,TRedisPoolConfig,TPooledRedisClient
Thread Safety
- ✅
IRedisConnectionPool— fully thread-safe (TCriticalSection) - ✅ Each
TPooledConnection— dedicated lock for concurrent acquisitions - ✅ Publisher and Subscriber — dedicated, isolated TCP connections
- ✅
TRedisPubSubListenerThread— background listening without blocking the app - ✅
AcquireConnectionOnDb— safe SELECT for every thread - ✅
TRedisGrijjyTransport— receive buffer protected byTCriticalSection+TEvent - ⚠️ Base
IRedisClient— NOT thread-safe: use the pool in multi-threaded applications
🔒 Error Handling
try
lClient.Connect;
lClient.SetValue('test', 'value');
except
on E: ERedisTimeoutException do // TCP/read timeout
WriteLn('Timeout: ' + E.Message);
on E: ERedisConnectionException do // TCP/SSL connection error
WriteLn('Connection: ' + E.Message);
on E: ERedisProtocolException do // malformed RESP response
WriteLn('Protocol: ' + E.Message);
on E: ERedisCommandException do // Redis error (-ERR ...)
WriteLn('Command: ' + E.Message);
on E: ERedisPipelineException do // error in pipeline mode
WriteLn('Pipeline: ' + E.Message);
on E: ERedisValidationException do // invalid parameter on client side
WriteLn('Validation: ' + E.Message);
on E: ERedisConfigException do // invalid configuration
WriteLn('Config: ' + E.Message);
on E: ERedisException do // catch-all
WriteLn('Redis: ' + E.Message);
end;⚙️ Advanced Configuration
TRedisConnectionConfig auto-initializes with safe values as soon as it is declared.
var lConfig: TRedisConnectionConfig;
// Already ready: host=localhost, port=6379, timeout=5000ms, maxRetries=3...
lConfig.Host := 'redis.example.com';
lConfig.Port := 6379;
lConfig.Username := 'admin';
lConfig.Password := 'secret';
lConfig.Database := 2;
lConfig.UseSSL := True;
lConfig.ValidateSSLCertificate := True;
lConfig.ClientCertificatePath := 'C:\certs\client.pem';
lConfig.ClientCertificatePassword := 'certpass';
lConfig.ConnectionTimeout := 10000;
lConfig.ReadTimeout := 30000;
lConfig.WriteTimeout := 30000;
lConfig.MaxRetries := 3;
lConfig.RetryDelayMs := 100;
lConfig.KeepAlive := True;
lConfig.KeepAliveInterval := 30;
lConfig.ClientName := 'my-app';
lConfig.ClusterMode := True;
lConfig.ClusterNodes := 'node1:6379,node2:6379';
lConfig.Validate; // raises ERedisConfigException if something is wrong
WriteLn(lConfig.ToConnectionString);
// → redis.example.com:6379, user=admin, password=***, db=2, ssl=true, name=my-app✅ Best Practices
Connections
- Use connection pooling for any application with concurrent requests
- Always close with Disconnect in
finally, or useTPooledRedisClient - Use
AcquireConnectionOnDbinstead of manualSelecton pooled connections
Transport
- Use Synapse for Windows-only applications: it is the default and requires no extra setup
- Use Grijjy for cross-platform applications or when native SSL without external DLLs is required
- Add only the transport unit you need to the project: registration is automatic
IRedisSynapseTransportis treated as an alias forIRedisTransport— existing code works without changes
Performance
- Use Pipeline for batch operations: N round-trips → 1
- Use SCAN instead of KEYS on large datasets — KEYS is blocking
- Use SSCAN/HSCAN/ZSCAN on very large collections
- Use UNLINK instead of DEL for non-blocking deletions
- Use HyperLogLog for high-cardinality unique counts
Security
- Always use SSL/TLS in production
- Do not hardcode credentials — use encrypted files or environment variables
- Use Redis 6+ ACL for the principle of least privilege
- Call
ValidatebeforeConnect
Logging
- Implement
IRedisLoggerwith a dedicated system (file, syslog, OpenTelemetry) TSimpleRedisLoggeris for development and testing only- Passwords are masked automatically in logs
🗺️ Roadmap
Completed in v2.0 ✅
- [x] All data types: String, List, Set, Hash, Sorted Set, HyperLogLog
- [x] Advanced commands: MGET/MSET/MSETNX, GETSET, GETDEL, GETEXPERSIST, COPY, UNLINK, SMISMEMBER, LMOVE, ZPOPMIN/MAX, ZRANDMEMBER, HRANDFIELD, ZADDOPT
- [x] Advanced TTL: PEXPIRE, PTTL, EXPIREAT, PEXPIREAT, EXPIRETIME, PEXPIRETIME (Redis 7+)
- [x] Pipeline — BeginPipeline / PipelineCommand / ExecPipeline
- [x] Lua Scripting — EVAL, EVALSHA, SCRIPT LOAD
- [x] WATCH / UNWATCH — optimistic locking
- [x] HyperLogLog — PFADD, PFCOUNT, PFMERGE
- [x] Server/Admin: DBSIZE, FLUSHDB, INFO, SELECT, CONFIG GET/SET, CLIENT SETNAME/GETNAME
- [x] Connection Pool with
AcquireConnectionOnDband automatic SELECT fix - [x]
TPooledRedisClient— managed record with auto-release - [x]
TRedisThreadHelper— helper for multi-thread load testing - [x]
TRedisScanResult— cursor + items for SCAN/SSCAN/HSCAN/ZSCAN - [x] Typed exception hierarchy: ERedisTimeout, ERedisPipeline, etc.
- [x]
TRedisPoolStatswith detailed metrics - [x] SSL/TLS with mTLS
Completed in v2.1 ✅
- [x] Unified
IRedisTransport— replacesIRedisSynapseTransport(alias kept for backward compatibility) - [x]
TRedisTransportRegistry— global registry with auto-registration viainitialization - [x] Grijjy transport —
TRedisGrijjyTransportcross-platform with async I/O and native SSL - [x]
TRedisClient.Newwith optionalaProviderparameter ('synapse'default,'grijjy'available) - [x] Linux/macOS/iOS/Android support via the Grijjy provider
Planned
- [ ] Redis Streams (XADD, XREAD, XGROUP, XACK)
- [ ] Geospatial (GEOADD, GEODIST, GEORADIUS, GEOSEARCH)
- [ ] Redis Cluster (automatic slot routing)
- [ ] Redis Sentinel (automatic failover)
- [ ] RESP3 (native types: map, set, double)
- [ ] Async/Await pattern (FireMonkey)
- [ ] Integrated benchmark tools
🤝 Contributing
- Fork the project
- Create a Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push (
git push origin feature/AmazingFeature) - Open a Pull Request
Guidelines
- Naming:
lprefix for local variables,Ffor private fields,afor parameters - Add unit tests for every new feature
- Update documentation for changes to the public API of
IRedisClient - Test on Delphi 10.4+ and on Redis 7.x and 8.x
- Maintain backward compatibility of
IRedisClient - For new transports: implement
IRedisTransport, auto-register in theinitializationsection
📄 License
Distributed under the Apache License 2.0 — see LICENSE for the full text.
Copyright 2025-2026 Muzio Valerio
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0👏 Acknowledgements
- Synapse — TCP/IP library for Delphi (transport
synapse) - GrijjyFoundation — Cross-platform TCP/IP library for Delphi (transport
grijjy) - Redis — The in-memory database
- Delphi community for support and feedback
📞 Support
- 🐛 Bug Reports: GitHub Issues
- 💡 Feature Requests: GitHub Discussions
📊 Stats
Made with ❤️ for the Delphi Community

