RESP2 or RESP3. A subset of Redis Streams commands is supported, with S2’s durability, consistency, and retention semantics.
Why Redis Streams on S2?
If you use Redis Streams to deliver live output, S2 keeps that output available for readers who disconnect, fall behind, or come back later. You get:- durable writes
- streams that grow without bound, not limited by memory
- serverless pricing
AI responses and agent runs
Stream token chunks, tool calls, and progress updates, and resume missed output when a user returns.
Build logs and background jobs
Stream build, import, or sandbox logs, and debug a completed or failed job later.
Activity feeds
Stream customer, device, or session events, and let each reader catch up at its own pace.
When to use something else
- Other Redis features: only part of Redis Streams is supported, and no other Redis data types, transactions, scripting, or pub/sub. In particular, there are no consumer groups, explicit
XADDIDs, exact trimming,XDEL,XREVRANGE, orXINFO. See command support. - S2 features beyond Redis: conditional appends, fencing tokens, and encrypted streams need the native S2 SDKs or API.
- In-memory latency: S2 acknowledges a write once it is durable in multiple availability zones, within 40 ms on Express and 400 ms on Standard from the same region. If you need sub-millisecond latency, use an in-memory Redis.
Command support
Keys are UTF-8 stream names in the connection’s basin. Only the forms below are supported; command names link to the Redis reference.Connecting
Connect to{basin}.r.s2.dev:6380 over TLS 1.3, sending the same hostname as TLS SNI; connections without SNI are rejected. Clients that take a URL should use rediss://.
You’ll need a basin and an access token. The examples write to a stream called events, so create it or enable create_stream_on_append on the basin.
With redis-cli:
Client libraries
Each example appends three entries toevents, reads the stream from the start one page at a time, then tails it with XREAD BLOCK. They read S2_BASIN and S2_ACCESS_TOKEN from the environment.
Python, Node.js, and Go examples
Python, Node.js, and Go examples
- Keep the client’s read timeout longer than
BLOCK. redis-py needs an explicitsocket_timeout; go-redis extends its read deadline automatically. - A blocking read holds its connection, so clients that share one connection across commands, such as node-redis and ioredis, need a separate connection for it.
Authentication and permissions
Each connection is tied to the basin in its hostname,
{basin}.r.s2.dev, which must also be sent as TLS SNI. Only streams in that basin are reachable; use a separate connection for each basin.AUTH S2_ACCESS_TOKEN; in a client library, set the token as the password. No username is needed, though default or the basin name work if your client sends one. Authenticate within 30 seconds of connecting.
AUTH only checks that the token is valid. Each command is then authorized against the token’s scope: the basin, the stream names, and the operations the command uses. Anything the token doesn’t allow replies NOPERM. With auto-prefixing, keys are prefixed too: with the prefix user1/, key logs is stream user1/logs.
Basin and stream configuration
Basins and streams are configured through the dashboard, CLI, or API, not through Redis. The settings that matter here:- Creation: create streams up front, or enable the basin’s
create_stream_on_appendsoXADDcreates them. New streams use the basin’s default stream config. - Retention: defaults to 7 days;
infinitekeeps everything. Retention beyond the free-tier limit requires a payment method. - Storage class: Express has lower latency than Standard; both are equally durable.
- Timestamping: Redis appends carry no client timestamp, so use
arrivalor the defaultclient-prefer;client-requirerejects them. - Encryption: encrypted streams need a key on each request, which Redis commands can’t pass, so they can’t be used here.
Creation on read and NOMKSTREAM
For a stream that doesn’t exist yet:
NOMKSTREAM checks for the stream before appending, and with create_stream_on_read the check itself creates it. The reads made by XLEN, XTRIM, and inline trimming can also create streams. Because the check and the append are separate, a stream deleted in between can be recreated by create_stream_on_append.
Command permissions
A token needs the operations a command uses in itsscope.ops. The table lists what each command can call; with batching and caching, not every call happens every time.
A few details:
AUTHaccepts a permission error from itslist-streamscall, so tokens limited to stream operations can still authenticate.- Grant
check-tailalongsideread. A token with onlyreadworks until a read needs to check the tail. trimauthorizes appending a trim record, so trimming needs bothtrimandappend. A trim with nothing to remove may use neither.- Streams created by the basin’s creation settings don’t need the
create-streampermission.
stream.read covers read and check-tail; stream.write covers append, trim, and fence; and basin.read covers get-stream-config and list-streams.
Core semantics
XADD
XADD key * field value ... returns an ID once the record is durable in multiple availability zones, on either storage class.
IDs are always generated, as <timestamp>-<seq_num>: Unix milliseconds, then the record’s S2 sequence number. Sequence numbers only increase; they don’t reset when the timestamp changes or after trimming.
Each record can be up to 1 MiB in metered bytes. For n field/value pairs:
Ordering and retries
PipelinedXADDs on one connection are appended in order per stream, and replies come back in request order. Appends to different streams, or from different connections, can become visible in any order. Other commands wait for earlier XADDs on their connection to finish. A pipeline is not a transaction.
If the connection drops before an
XADD is acknowledged, the write may or may not have happened. Retrying it can create a duplicate.XREAD and XRANGE
Reads return entries in stream order and are linearizable: once an XADD is acknowledged, later reads see it. An XREAD across several streams is not a snapshot of all of them at one point in time.
Pagination and the tail
A page with fewer thanCOUNT entries doesn’t mean you’ve caught up. Pages can end early because of the 10,000-entry cap or size limits. XREAD also splits the cap evenly across its streams, so with 100 streams each returns at most 100 entries.
Keep reading until you get an empty result:
XRANGE: start the next page at(last-id. An empty array means the range is exhausted.- Nonblocking
XREAD: pass each stream’s last returned ID, keeping the previous cursor for streams that returned nothing.nullmeans nothing more is available yet.
1713812735000-42:
$ means “from the current tail”, so using it again between reads can skip entries; continue from the returned IDs instead.
Blocking reads
XREAD BLOCK milliseconds waits for new entries; BLOCK 0 waits indefinitely. A finite wait includes setting up the read, so a short one can return null even when entries exist. Use a nonblocking read to check whether you’ve caught up, and after a timeout, retry with the same cursors.
Set the client’s socket timeout longer than the wait, or disable it for BLOCK 0; redis-py 8, for example, defaults to 5 seconds. Use a separate connection if other commands need to run while a read blocks.
Tailing a stream
Once a consumer catches up, useXREAD BLOCK instead of polling. An empty XRANGE or nonblocking XREAD near the tail can cost a billed check-tail; polling a quiet stream every 100 ms adds about 864,000 operations a day. XREAD BLOCK on one stream waits on its open read without tail checks. On several streams, it checks each quiet stream once per command, so longer waits cost less.
XTRIM and XLEN
Only approximate trimming is supported: MAXLEN ~ threshold and MINID ~ id. A trim appends a command record, which appears in reads as an entry with an empty field array and counts toward COUNT, MAXLEN, and XLEN. Other command records, such as fences, and records written natively without the Redis encoding appear the same way.
Trimming is eventual: older entries can stay readable for a while after XTRIM returns. Its return value estimates how many entries the trim will remove.
For example, with three entries A, B, and C, XTRIM events MAXLEN ~ 2 returns 2:
[] is the trim record. It has its own ID, so continue from it like any other entry. This means MAXLEN ~ 1 can leave only the trim record, and MAXLEN ~ 0 can eventually leave the stream empty.
Inline XADD trimming appends the entry and the trim record atomically, but concurrent writers can still leave more than MAXLEN entries.
XLEN counts retained entries, including trim records, and can be briefly off while trimming or retention takes effect. A stream can be empty and still exist: XLEN returns 0 while EXISTS returns 1. delete_on_empty can remove such streams.
Retention removes old entries independently of XTRIM. Once removed, entries can’t be read through any interface.
Interoperability with S2 APIs
Redis and native S2 clients can use the same streams. Trimming, retention, and deletion affect both.IDs and positions
In1713812735000-42, 1713812735000 is the record’s timestamp and 42 is its S2 sequence number. To resume natively after this entry, read from sequence number 43. Timestamps pass through unchanged, so if native writers use a unit other than milliseconds, Redis IDs do too.
Message encoding
XADD writes each field/value pair as a record header, in order, after a first header s2-resp.v: 1. The body is empty:
--format json-base64 for fields or values that aren’t valid UTF-8.
Fields and values are binary-safe, and pair order and duplicates are preserved. Headers can’t have empty names, so a field name that is empty or starts with a NUL byte is stored with one extra leading NUL byte, which is removed on read.
Native writers can produce Redis entries the same way. Only the first header marks a Redis entry; a later s2-resp.v header is an ordinary field. Records without the marker appear in Redis as entries with no fields. A record with the marker and a non-empty body causes a read error.
Concurrency controls
Append conditions (match_seq_num and fencing tokens) are only available through the native API. Fencing doesn’t stop Redis writers, because their appends never carry a token.
Performance and quality of service
Latency depends on the storage class and your distance from the basin; see append and read latency. For throughput, pipelineXADDs: the service batches them per stream and connection. NOMKSTREAM and inline trimming take extra round trips, so benchmark them separately.
Reuse connections and keep pipeline depth bounded. Fewer, busier connections batch better and can reuse read sessions. Give blocking readers their own connections, since a slow reply holds up later replies on the same connection.
Under load, the service can slow down accepting commands, reply TRYAGAIN, or close connections. Reconnect with backoff.
Errors and reconnection
Other limits:
- A command must arrive in full within 30 seconds and finish within 30 seconds.
XREAD BLOCKadds its wait to that;BLOCK 0has no limit. A command that runs too long closes the connection without a reply. - A command can be at most 2 MiB with 16,384 arguments. Many small fields can hit these limits before the 1 MiB record limit.
- On restart or scale-in, connections drain for up to 20 seconds and then close. Treat writes without replies as ambiguous.
Billing
Usage is billed at S2 pricing for storage, writes, reads, and operations, including the Redis encoding overhead and trim records. Redis commands don’t map one-to-one to operations: manyXADDs can share one append, and some commands make several calls. The permissions table lists the operations each command can make; this includes one list-streams call per connection when it authenticates.
Retaining history
Consider 100 events per second across 32 streams, each with a 1 KiB value and read once by one consumer per stream. Each column is the estimated monthly cost of retaining that much history:
These are illustrative cost estimates for this workload, not measured hosted bills or a benchmark of equivalent latency. Rates were checked on September 28, 2026, using a 730-hour month and AWS on-demand pricing in
us-east-1. Sources: S2, MemoryDB, and ElastiCache.
Workload and sizing assumptions
Workload and sizing assumptions
The workload uses 32 equal-rate streams, one persistent writer and one dedicated blocking reader per stream. Each event has one field,
d, and a 1,024-byte value. The consumer reads every event once without additional historical replay. Retention is at steady state after the full retention period has accumulated.S2’s estimate includes 1,057 metered bytes per record, retained storage, writes, internet-priced reads, session operations, and resource and authentication charges. It assumes connections and read sessions remain reusable. At 30 days on Express, that is about $13 for storage, $19 for writes, $26 for reads, and $3 for operations and resources.AWS sizing uses approximately 1,385 bytes per record measured in a local Valkey 8.1.1 stream. This is a sizing proxy; hosted memory usage and throughput have not been measured. The model assumes balanced key placement, with 20% memory headroom for MemoryDB and ElastiCache’s default 25% memory reserve.Each column uses the cheapest fitting on-demand configuration. MemoryDB uses five db.t4g.medium shards at 1 day and one db.r6g.4xlarge at 7 days. At 30 days, it uses one db.r6g.16xlarge, which costs $6.8957 per hour. MemoryDB persists writes to a Multi-AZ transaction log, so replicas are optional; adding one replica per shard for availability doubles its estimate. ElastiCache uses one cache.r6g.xlarge shard at 1 day, one cache.r6g.4xlarge shard at 7 days, and two cache.r8g.8xlarge shards at 30 days, priced including the synchronous-durability premium. Synchronous durability requires a replica per shard, so every ElastiCache estimate includes one. ElastiCache’s synchronous durability acknowledges writes after persisting them to a Multi-AZ transaction log; this mode does not support Serverless or data tiering. The MemoryDB stream keys exceed its 128 MiB tiering threshold, so the model keeps their history in memory.Estimates exclude taxes, promotional credits, commitment discounts, additional snapshots, and application-side compute and network charges. Different memory footprints, reconnects, polling, and unused read-ahead can change the result.XTRIM and run a consumer that archives entries to object storage. With a 1-day window, that costs roughly the 1-day column above plus about $6 a month to keep 247 GiB in S3 Standard. The cost is operational: an archiver to run and monitor, two read paths, and replays that must hand off from the archive to the stream at the right ID without gaps or duplicates. With S2, the history stays in the stream and every reader uses the same commands.
Shorter retention or frequent rereading narrows the cost gap. Compare latency and throughput requirements as well as cost.

