Commands
Supported Redis commands — full matrix, notes, and TTL behaviour.
PostRESP implements a growing Redis command surface over Postgres storage. Status meanings:
| Badge | Meaning |
|---|---|
| Supported | Implemented with Redis-like semantics for the common path |
| Partial | Works for basic usage; some options, types, or edge cases missing |
| Stub | Minimal reply for client/harness compatibility — not a full implementation |
| Not supported | Returns an unknown-command error today |
Redis compatibility is validated against a pinned Valkey test suite (currently 8.1.4). See the live per-test tracker on Compatibility.
Command matrix
Connection & server
| Command | Status | Notes |
|---|---|---|
PING |
Supported | |
ECHO |
Supported | |
QUIT |
Supported | |
AUTH |
Supported | Postgres LOGIN role; required by default (RequireClientAuth) |
SELECT |
Partial | Only database 0 |
INFO |
Supported | default / all / named sections (cpu, replication, commandstats, …); redis_version matches the Valkey pin; pg_resp_* fields |
HELLO |
Partial | RESP2 only (HELLO / HELLO 2); HELLO 3 → NOPROTO; supports AUTH / SETNAME options |
CLIENT |
Partial | REPLY, SETNAME, GETNAME, SETINFO (lib-name / lib-ver) |
CONFIG |
Partial | GET returns real values (databases=1, …); SET accepted as no-op except databases; RESETSTAT clears INFO counters |
FUNCTION |
Stub | FLUSH accepted for harnesses |
COMMAND |
Stub | Empty array reply |
FLUSHDB |
Supported | |
FLUSHALL |
Supported |
Strings & keys
| Command | Status | Notes |
|---|---|---|
SET |
Supported | NX / XX / GET / IFEQ / EX / PX / EXAT / PXAT / KEEPTTL |
SETNX |
Supported | |
SETEX / PSETEX |
Supported | |
GET |
Supported | Binary-safe; empty string ≠ missing key |
GETDEL |
Supported | |
GETEX |
Supported | |
GETSET |
Supported | |
DEL |
Supported | |
UNLINK |
Supported | Same as DEL today (synchronous delete; reply is deleted count) |
EXISTS |
Supported | |
TYPE |
Supported | none / string / list / set (hash when hash storage exists) |
KEYS |
Partial | Redis glob MATCH; expensive — prefer SCAN for large keyspaces |
RANDOMKEY |
Supported | Null bulk when DB is empty |
STRLEN |
Supported | |
APPEND |
Supported | |
GETRANGE / SUBSTR |
Supported | SUBSTR aliases GETRANGE |
SETRANGE |
Supported | |
MGET / MSET / MSETNX |
Supported | |
INCR / DECR / INCRBY / DECRBY |
Supported | |
INCRBYFLOAT |
Supported | |
DBSIZE |
Supported | |
MEMORY USAGE |
Partial | Approximate payload size (key + value) |
SETBIT / GETBIT |
Supported | |
LCS |
Supported |
Hashes
| Command | Status | Notes |
|---|---|---|
HSET / HMSET |
Supported | Variadic field/value pairs; HMSET is legacy OK reply |
HSETNX |
Supported | |
HGET / HMGET |
Supported | Missing fields → null bulk |
HDEL |
Supported | Empty hash deletes the key |
HEXISTS / HLEN |
Supported | |
HGETALL / HKEYS / HVALS |
Supported | |
Other hash commands (HINCRBY, HSCAN, HRANDFIELD, …) |
Not supported |
Sets
| Command | Status | Notes |
|---|---|---|
SADD |
Supported | Returns newly added member count |
SREM |
Supported | Deletes key when the set becomes empty |
SISMEMBER |
Supported | |
SMISMEMBER |
Supported | |
SCARD |
Supported | |
SMEMBERS |
Supported | |
SPOP |
Partial | Optional count; random member(s); no replication rewrite |
SUNION |
Supported | Missing keys treated as empty |
Other set commands (SINTER, SDIFF, SSCAN, …) |
Not supported |
Lists
| Command | Status | Notes |
|---|---|---|
LPUSH / RPUSH |
Supported | Variadic |
LPOP / RPOP |
Supported | Optional count |
LLEN |
Supported | |
LRANGE |
Supported | |
LINDEX |
Supported | |
LTRIM |
Supported | Empty result deletes the key |
LSET |
Supported | ERR no such key / ERR index out of range |
LREM |
Supported | Count head/tail/all semantics |
LINSERT |
Supported | BEFORE / AFTER |
Blocking / move (BLPOP, LMOVE, …) |
Not supported |
TTL / expiry
| Command | Status | Notes |
|---|---|---|
EXPIRE / PEXPIRE |
Supported | NX / XX / GT / LT; lazy + active purge |
EXPIREAT / PEXPIREAT |
Supported | Absolute unix seconds / milliseconds |
EXPIRETIME / PEXPIRETIME |
Supported | |
TTL / PTTL |
Supported | |
PERSIST |
Supported | |
TIME |
Supported | Server clock for clients / harnesses |
DELEX |
Supported | IFEQ only (safe lock unlock); not IFNE / digest |
SCAN |
Supported | Cursor + MATCH / COUNT / TYPE (string/list/set/zset/hash/stream) |
Pub/Sub
| Command | Status | Notes |
|---|---|---|
PUBLISH |
Supported | In-process fan-out (single gateway instance) |
SUBSCRIBE / UNSUBSCRIBE |
Supported | RESP2 subscribed-mode filter; RESP2 PING → ["pong", …]; RESP3 PING normal |
PSUBSCRIBE / PUNSUBSCRIBE |
Supported | Redis glob patterns (* / ? / \) |
PUBSUB |
Supported | CHANNELS / NUMSUB / NUMPAT / HELP |
CLIENT REPLY |
Supported | ON / OFF / SKIP — silences command replies; pub/sub pushes still delivered |
CLIENT SETNAME / GETNAME |
Supported | Per-connection name |
CLIENT SETINFO |
Supported | lib-name / lib-ver (Lettuce handshake) |
Known limitation — process-local delivery
Pub/Sub fan-out is in-process only (one hub per gateway process). That matches the default single-instance / in-process RESP path: every client shares the same BGWorker.
If you later run multiple gateway processes against one Postgres (several
standalone redis_gateway pods, or more than one RESP listener), a PUBLISH on
instance A will not reach subscribers on instance B until we add a
cross-process bus — typically PostgreSQL LISTEN/NOTIFY on a single broadcast
channel. Key/value data already shared via SQL is unaffected; only live
subscription delivery is process-scoped today.
Not yet implemented (families)
| Area | Status | Notes |
|---|---|---|
| Hashes | Partial | Field basics; no incr/scan/rand yet |
| Sets | Partial | Basics + SUNION; no SINTER/SDIFF/SSCAN yet |
| Sorted sets | Not supported | |
| Streams | Not supported | |
| Keyspace notifications | Not supported | |
| Sharded pub/sub | Not supported | |
Transactions (MULTI / EXEC) |
Not supported | |
| Scripting / modules | Not supported | Beyond harness stubs |
| Clustering / replication admin | Not supported | Single logical DB (SELECT 0) |
Distributed locks
Single-node Redis-style locks (not Redlock / multi-node consensus) work without
Lua or MULTI/EXEC:
| Step | Command | Notes |
|---|---|---|
| Acquire | SET resource_name <unique-token> NX PX <ttl-ms> |
Null bulk if already held |
| Refresh | SET resource_name <unique-token> IFEQ <unique-token> PX <ttl-ms> |
Extends TTL only while you still hold the token |
| Unlock | DELEX resource_name IFEQ <unique-token> |
Integer 1 deleted / 0 if token mismatch or missing |
Use a random per-acquire token so an expired holder cannot delete another
client’s lock. Prefer DELEX IFEQ over DEL / GET+DEL. SQL storage
serializes per-key mutations (pgresp.lock_key) so concurrent SET NX and
DELEX IFEQ stay atomic across sessions.
TTL / expiry
- Lazy — expiry checked on read/write paths so stale keys are not returned.
- Active —
pg_cronperiodically batch-deletes expired rows.
Known limitation — naive active expiry
| Path | What we do now | Gap vs Redis |
|---|---|---|
SQL (pg_cron) |
Delete up to batch × rounds rows per minute |
Coarser than Redis sampling cadence |
SQL sweeper GUCs:
| GUC | Default | Meaning |
|---|---|---|
pg_resp.ttl_delete_batch_size |
1000 |
Keys per DELETE round |
pg_resp.ttl_delete_max_rounds |
10 |
Rounds per CALL pgresp.delete_expired_keys() |
Correctness (lazy-on-access + eventual active delete) is fine. Cost under very large key counts is not — raise the GUCs to drain backlogs faster, or prefer access patterns that rely on lazy expiry.