Skip to content
PostRESP
Esc
↑↓navigate↵open⌘Jpreview
On this page

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; optional by default (RequireClientAuth: false)
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
HINCRBY Supported Integer field increment
Other hash commands (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
LPOS Supported Optional RANK
RPOPLPUSH Supported
BZPOPMIN Supported Process-local key notify (like in-process pub/sub)
Blocking list ops (BLPOP, LMOVE, …) Not supported

Sorted sets

Command Status Notes
ZADD Partial Score/member pairs; no NX/XX/GT/LT/CH/INCR
ZREM / ZCARD / ZSCORE Supported
ZRANGE / ZREVRANGE Partial Optional WITHSCORES
ZRANGEBYSCORE / ZREVRANGEBYSCORE Partial LIMIT; WITHSCORES
ZCOUNT / ZPOPMIN Supported
ZREMRANGEBYRANK / ZREMRANGEBYSCORE Supported
BZPOPMIN Supported Non-blocking inside MULTI/EVAL

Streams

Command Status Notes
XADD Partial Auto-ID *; optional MAXLEN / ~
XTRIM Partial MAXLEN only
XREAD Partial COUNT / BLOCK; $ latest; no consumer groups
Consumer groups / XACK / … 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.

Scripting

Command Status Notes
EVAL Partial Lua 5.1; KEYS/ARGV; redis.call/pcall; sandboxed (no os/io/package); runs in a Postgres transaction when storage is available
EVALSHA Partial Process-local SHA1 cache; NOSCRIPT when missing
SCRIPT LOAD / EXISTS / FLUSH Supported In-process cache (like Redis); FLUSH ASYNC|SYNC accepted
SCRIPT KILL / DEBUG Stub KILL → NOTBUSY; DEBUG → OK
cjson.encode / decode Partial Minimal JSON helper for common scripts (BullMQ-oriented)
cmsgpack.pack / unpack Partial Redis lua_cmsgpack types (nil → Lua nil, not userdata); unpack_one / unpack_limit; bin strings accepted
Nested EVAL / pubsub-subscribe / MULTI from scripts Not supported Rejected like Redis

Transactions

Command Status Notes
MULTI / EXEC / DISCARD Partial Queues on the connection; EXEC runs one Postgres txn. No WATCH CAS abort
WATCH / UNWATCH Stub No-ops — Postgres isolation covers MULTI/EXEC

Not yet implemented (families)

Area Status Notes
Hashes Partial Field basics + HINCRBY; no scan/rand yet
Sets Partial Basics + SUNION; no SINTER/SDIFF/SSCAN yet
Sorted sets Partial Core read/write + BZPOPMIN; limited ZADD options
Streams Partial XADD/XREAD/XTRIM; no consumer groups
Keyspace notifications Not supported
Sharded pub/sub Not supported
Scripting extras Partial EVAL/EVALSHA/SCRIPT + cjson/cmsgpack; not full Redis Lua libs
Clustering / replication admin Not supported Single logical DB (SELECT 0)

Distributed locks

Single-node Redis-style locks work without Lua or with classic Redlock Lua:

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 (native) DELEX resource_name IFEQ <unique-token> Integer 1 deleted / 0 if token mismatch or missing
Unlock (Lua) EVAL compare-and-DEL (Infisical / node-redlock style) Supported via EVAL/EVALSHA

Use a random per-acquire token so an expired holder cannot delete another client’s lock. Prefer DELEX IFEQ over DEL / GET+DEL when you control the client; Lua unlock scripts used by Redlock libraries also work. SQL storage serializes per-key mutations (pgresp.lock_key) so concurrent SET NX and DELEX IFEQ stay atomic across sessions. Scripted unlocks run inside a Postgres transaction for the duration of EVAL.

TTL / expiry

  1. Lazy — expiry checked on read/write paths so stale keys are not returned.
  2. Active — pg_cron periodically 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.

Was this page helpful?