resumable-stream

Lets serverless servers resume or share a streaming HTTP response across client reconnects using Redis pub/sub.

Library
npm
v2.2.12
570stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
45/100Fair
Development Activity52
Maintenance4
Community44
Maturity40
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
76/100Good
Architecture78
Code Quality82
Innovation68
Learning Curve75

resumable-stream solves a specific problem for streaming HTTP responses (like SSE) in serverless environments without sticky load balancing: if the client disconnects mid-stream, or a second client wants to follow along, the original stream has already moved on. The library wraps a ReadableStream<string> so the first request becomes the producer — it always runs to completion even if its own reader disconnects — while publishing each chunk over Redis pub/sub. Later requests for the same streamId become consumers: they replay the buffered chunks from where they left off (via a skipCharacters offset) and then continue receiving new chunks live.

It ships three entry points backed by one shared core: resumable-stream (the redis package, connecting from REDIS_URL/KV_URL), resumable-stream/ioredis (adapts a raw ioredis client automatically), and resumable-stream/generic (bring your own Publisher/Subscriber, for Upstash Redis, Valkey, or anything Redis-like). A durable sentinel key tracks whether a stream is in-progress or done, and a watchdog periodically re-checks that sentinel so a resumed stream still closes correctly even if the fire-and-forget DONE pub/sub message is lost.

Built by Vercel and used inside the Vercel AI SDK’s chat examples for resumable AI response streaming, but it’s generic to any use case needing stream recovery — not tied to LLM output specifically.

What You Get

  • Idempotent resumableStream() - one call that transparently starts a stream on first request and resumes it on any subsequent request for the same ID.
  • Explicit createNewResumableStream / resumeExistingStream - split API for callers who want to control the create-vs-resume decision themselves (e.g. POST creates, GET resumes).
  • Three ready-made backends - default redis client, an ioredis adapter, and a generic mode for any Publisher/Subscriber-shaped client (Upstash, Valkey, etc.).
  • hasExistingStream() - check whether a stream ID is in progress, done, or unknown without attaching a reader.
  • Character-offset resumption - skipCharacters lets a reconnecting client request only the content it’s missing instead of the full buffered stream.
  • DONE-sentinel watchdog - periodically re-verifies the durable completion sentinel so a resumed stream still closes if the one-shot pub/sub DONE message is dropped.

Common Use Cases

  • Resumable AI chat responses - an LLM streams its answer to a serverless function; if the browser tab reloads mid-generation, the client reconnects and picks up the response instead of losing it.
  • Multi-tab / multi-device follow-along - a second browser tab or device opens the same stream ID and receives the same buffered-then-live content as the original requester.
  • Long-running SSE behind non-sticky load balancers - any platform (Vercel, other FaaS) that can route a reconnect to a different instance than the one producing the stream.
  • Decoupling stream production from delivery - the producer function keeps writing to Redis even after its own HTTP response finishes, via waitUntil, so slow consumers don’t block the producer’s lifecycle.

Under The Hood

Architecture The library is organized as one shared core (createResumableStreamContextFactory in src/runtime.ts) parameterized by a RedisDefaults object, with three thin entry modules — src/redis.ts, src/ioredis.ts, src/generic.ts — that each supply a different default publisher/subscriber pair into the same factory. ioredis-adapters.ts translates a raw ioredis client into the library’s own Publisher/Subscriber interfaces so the core logic never has to know which client shape it’s talking to. The runtime itself tracks stream state through two Redis primitives: a sentinel key (an incrementing counter that also encodes a DONE string state) and channel-based pub/sub for chunk delivery and resume-request signaling, with a per-listener watchdog timer in resumeStream re-checking the sentinel as a fallback if the fire-and-forget DONE message never arrives. This is a clean case of one core module reused across multiple adapters rather than duplicated per-backend logic.

Tech Stack Written in TypeScript targeting the DOM ReadableStream API, with redis v4 and ioredis v5 as optional client dependencies and next referenced only for its waitUntil-shaped lifecycle hook, not as a hard dependency. Builds with tsc against a dedicated tsconfig.build.json, tests run under vitest, and API docs are generated into the checked-in docs/ directory via typedoc + typedoc-plugin-markdown. Package manager is pinned to a specific pnpm version via packageManager in package.json.

Code Quality The test suite (src/__tests__/tests.ts) is written once as a shared resumableStreamTests function and then run three times — in-memory, against ioredis, and against redis — against a common in-memory pub/sub fake (testing-utils/in-memory-pubsub.ts) and a real-Redis path gated behind a REDIS_URL secret in CI, giving good behavioral coverage across all three backends without triplicating test logic. GitHub Actions runs both a PR workflow (no live Redis) and a main-branch workflow (with live Redis) on every change. Error handling favors explicit try/catch around cleanup paths with debug-gated logging (DEBUG=1 env var) rather than silent swallowing, and public types are fully annotated with JSDoc comments describing return-value semantics (null vs undefined vs a stream). No linter config is present beyond Prettier for formatting.

API Design The public surface is deliberately small: one factory call per backend, three stream-lifecycle methods, and one status check. The Publisher/Subscriber interfaces are intentionally shaped to match both redis and ioredis client method signatures directly, so generic mode consumers can often wire up an existing client with minimal adapter code. Return-value semantics (null = stream already finished, undefined = no stream exists, a stream = actively producing or resuming) are documented inline via JSDoc on every public method, reducing the need to read source to understand edge-case behavior.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search