Skip to main content
Use Redis clients to append, replay, and tail S2 streams over RESP2 or RESP3. A subset of Redis Streams commands is supported, with S2’s durability, consistency, and retention semantics.
This integration is currently in beta.

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: For workloads that keep history around, that can be an order of magnitude or more cheaper than an equivalent provisioned Redis. Good fits include:

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 XADD IDs, exact trimming, XDEL, XREVRANGE, or XINFO. 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.
and share it with your coding agent.Ask it to review your app’s Redis usage, flag compatibility gaps, and assess whether S2 fits your workload.

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:
Then, in the CLI:

Client libraries

Each example appends three entries to events, 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.
Client notes:
  • Keep the client’s read timeout longer than BLOCK. redis-py needs an explicit socket_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.
Authenticate with 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_append so XADD creates them. New streams use the basin’s default stream config.
  • Retention: defaults to 7 days; infinite keeps 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 arrival or the default client-prefer; client-require rejects 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 its scope.ops. The table lists what each command can call; with batching and caching, not every call happens every time. A few details:
  • AUTH accepts a permission error from its list-streams call, so tokens limited to stream operations can still authenticate.
  • Grant check-tail alongside read. A token with only read works until a read needs to check the tail.
  • trim authorizes appending a trim record, so trimming needs both trim and append. A trim with nothing to remove may use neither.
  • Streams created by the basin’s creation settings don’t need the create-stream permission.
Alternatively, grant operation groups: 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:
With inline trimming, the record and its trim record must fit in 1 MiB together.

Ordering and retries

Pipelined XADDs 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.
The service retries an append only when it knows the earlier attempt had no effect; otherwise it closes the connection. There is no idempotency key over Redis. For stronger guarantees, use native conditional appends.

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 than COUNT 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. null means nothing more is available yet.
For example, after a page ending at 1713812735000-42:
Entries with no fields, such as trim records, are still entries: continue past their IDs. Store cursors in your application. $ 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, use XREAD 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

In 1713812735000-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:
For example, this entry:
reads natively as a record with those headers and no body:
Use --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, pipeline XADDs: 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 BLOCK adds its wait to that; BLOCK 0 has 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: many XADDs 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.
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.
In practice, teams running Redis Streams rarely keep a month of history in memory. Instead, they trim each stream to a short window with 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.

Read-session reuse

Reading a stream sequentially on one connection reuses the same S2 read session, which is billed by the minute. Reconnecting, jumping to a different cursor, or switching between blocking and nonblocking reads can open a new session. Data the service reads ahead is billed even if the client never requests it. Read transfer is priced by the client’s network path; connecting through the public endpoint doesn’t qualify for private-network pricing.