Streams require SDK version 4.1.0 or later (
@trigger.dev/sdk and @trigger.dev/react-hooks).
This doc describes the current streams behavior (v2 is the default). For pre-4.1.0 streams, see
Pre-4.1.0 streams (legacy) below.Overview
Streams provide:- Unlimited stream length (previously capped at 2000 chunks)
- Unlimited active streams per run (previously 5)
- Improved reliability with automatic resumption on connection loss
- 28-day stream retention (previously 1 day)
- Multiple client streams can pipe to a single stream
- Enhanced dashboard visibility for viewing stream data in real-time
x-trigger-realtime-streams-version=v2 header. To opt out, use auth.configure({ future: { v2RealtimeStreams: false } }) or TRIGGER_V2_REALTIME_STREAMS=0.
Limits Comparison
Quick Start
The recommended workflow for output streams (data from task to client):- Define your streams in a shared location using
streams.define() - Use the defined stream in your tasks with
.pipe(),.append(), or.writer() - Read from the stream using
.read()or theuseRealtimeStreamhook in React
Defining Typed Streams (Recommended)
The recommended way to work with streams is to define them once withstreams.define(). This allows you to specify the chunk type and stream ID in one place, and then reuse that definition throughout your codebase with full type safety.
Creating a Defined Stream
Define your streams in a shared location (likeapp/streams.ts or trigger/streams.ts):
Using Defined Streams in Tasks
Once defined, you can use all stream methods on your defined stream:Reading from a Stream
Use the defined stream’sread() method to consume data from anywhere (frontend, backend, or another task):
Appending to a Stream
Use the defined stream’sappend() method to add a single chunk:
Writing Multiple Chunks
Use the defined stream’swriter() method for more complex stream writing:
Using Defined Streams in React
Defined streams work seamlessly with theuseRealtimeStream hook:
Direct Stream Methods (Without Defining)
If you have a specific reason to avoid defined streams, you can use stream methods directly by specifying the stream key each time.Direct Piping
Direct Reading
Direct Appending
Direct Writing
Default Stream
Every run has a “default” stream, allowing you to skip the stream key entirely. This is useful for simple cases where you only need one stream per run. Using direct methods:Targeting Different Runs
You can pipe streams to parent, root, or any other run using thetarget option. This works with both defined streams and direct methods.
With Defined Streams
With Direct Methods
Streaming from Outside a Task
If you specify atarget run ID, you can pipe streams from anywhere (like a Next.js API route):
React Hook
Use theuseRealtimeStream hook to subscribe to streams in your React components.
With Defined Streams (Recommended)
With Direct Stream Keys
If you prefer not to use defined streams, you can specify the stream key directly:Using Default Stream
Hook Options
Input Streams
Input Streams let you send data into a running task from your backend or frontend. While output streams (above) send data out of tasks, input streams complete the loop — enabling bidirectional communication.Input Streams require SDK version 4.4.2 or later and use the same streams infrastructure (v2 is the default). If you’re on an older SDK, calling
.on() or .once() will throw with instructions to enable v2 streams. See Pre-4.1.0 streams (legacy) for the older metadata-based API.Input Streams overview
Input Streams solve three common problems:- Cancelling AI streams mid-generation. When you use AI SDK’s
streamTextinside a task, the LLM keeps generating until it’s done — even if the user clicked “Stop.” With input streams, your frontend sends a cancel signal and the task aborts the LLM call immediately. - Human-in-the-loop workflows. A task generates a draft, then pauses and waits for the user to approve or edit it before continuing.
- Interactive agents. An AI agent running as a task needs follow-up information from the user mid-execution — clarifying a question, choosing between options, or providing additional context.
Quick Start (Input Streams)
- Define input streams in a shared file with
streams.input<T>({ id: "..." }). - Receive in your task with
.wait(),.once(),.on(), or.peek(). - Send from your backend with
.send(runId, data)or from the frontend with theuseInputStreamSendhook (see Realtime React hooks).
Defining Input Streams
Usestreams.input() to define a typed input stream. The generic parameter controls the shape of data that can be sent:
.send() and the receiving methods (.wait(), .once(), .on(), .peek()) share the same type.
Receiving data inside a task
wait() — Suspend until data arrives
Suspends the task entirely, freeing compute resources. The task resumes when data arrives via .send(). Returns a ManualWaitpointPromise — the same type as wait.forToken().
.unwrap() to throw on timeout: const data = await approval.wait({ timeout: "24h" }).unwrap();
Options: timeout (e.g. "30s", "5m", "24h", "7d"), idempotencyKey, idempotencyKeyTTL, tags. Use idempotencyKey when your task has retries so the same waitpoint is resumed across retries.
once() — Wait for the next value (non-suspending)
Blocks until data arrives but keeps the task process alive. Returns a result object; use .unwrap() to get the data or throw on timeout.
once() also accepts a signal (e.g. AbortController.signal) for cancellation.
on() — Listen for every value
Registers a persistent handler that fires on every piece of data. Handlers are automatically cleaned up when the task run completes. Call .off() on the returned subscription to stop listening early.
peek() — Non-blocking check
Returns the most recent buffered value without waiting, or undefined if nothing has been received yet.
Sending data to a running task
Use.send(runId, data) from your backend to push data into a running task. See the backend input streams guide for API route patterns.
Complete example: Cancellable AI streaming
Stream an AI response while allowing the user to cancel mid-generation. Define the streams:cancelStream.on() to abort an AbortController, then pipe streamText(...).textStream to aiOutput. Backend: POST to an API route that calls cancelStream.send(runId, { reason: "User clicked stop" }). Frontend: Use useRealtimeStream(aiOutput, runId, { accessToken }) and a button that calls your cancel API (or use the useInputStreamSend hook; see Realtime React hooks).
Important notes (input streams): You cannot send to a completed, failed, or canceled run. Max payload per .send() is 1MB. Data sent before a listener is registered is buffered and delivered when a listener attaches; .wait() handles the buffering race automatically. Use .wait() for long waits to free compute; use .once() for short waits or concurrent work. Define input streams in a shared location and combine with output streams for full bidirectional communication.
Complete Example: AI Streaming
Define the stream
Create the task
Frontend component
Migration from v1
If you’re using the oldmetadata.stream() API, here’s how to migrate to the recommended v2 approach:
Step 1: Define Your Streams
Create a shared streams definition file:Step 2: Update Your Tasks
Replacemetadata.stream() with the defined stream’s pipe() method:
Step 3: Update Your Frontend
Use the defined stream withuseRealtimeStream:
Alternative: Direct Methods (Not Recommended)
If you prefer not to use defined streams, you can use direct methods:Reliability Features
Streams v2 includes automatic reliability improvements:- Automatic resumption: If a connection is lost, both appending and reading will automatically resume from the last successful chunk
- No data loss: Network issues won’t cause stream data to be lost
- Idempotent operations: Duplicate chunks are automatically handled
Dashboard Integration
Streams are now visible in the Trigger.dev dashboard, allowing you to:- View stream data in real-time as it’s generated
- Inspect historical stream data for completed runs
- Debug streaming issues with full visibility into chunk delivery
Best Practices
- Always use
streams.define(): Define your streams in a shared location for better organization, type safety, and code reusability. This is the recommended approach for all streams. - Export stream types: Use
InferStreamTypeto export types for your frontend components - Handle errors gracefully: Always check for errors when reading streams in your UI
- Set appropriate timeouts: Adjust
timeoutInSecondsbased on your use case (AI completions may need longer timeouts) - Target parent runs: When orchestrating with child tasks, pipe to parent runs for easier consumption
- Throttle frontend updates: Use
throttleInMsinuseRealtimeStreamto prevent excessive re-renders - Use descriptive stream IDs: Choose clear, descriptive IDs like
"ai-output"or"progress"instead of generic names
Pre-4.1.0 streams (legacy)
Prior to SDK 4.1.0, streams used the older metadata-based API. If you’re on an earlier version, see metadata.stream() for legacy usage. With 4.4.2+, Input Streams are available and documented in this page.Troubleshooting
Stream not appearing in dashboard
- Verify your task is actually writing to the stream
- Check that the stream key matches between writing and reading
Stream timeout errors
- Increase
timeoutInSecondsin yourread()oruseRealtimeStream()calls - Ensure your stream source is actively producing data
- Check network connectivity between your application and Trigger.dev
Missing chunks
- With the current streams implementation, chunks should not be lost due to automatic resumption
- Verify you’re reading from the correct stream key
- Check the
startIndexoption if you’re not seeing expected chunks
Input streams not working
- Input streams require SDK 4.4.2 or later and the default streams (v2) infrastructure. Ensure you’re on a recent SDK and not using the legacy metadata.stream() API.
- If
.on()or.once()throw, follow the error message to enable v2 streams (they are default in 4.1.0+).
”Stream is being deleted” during long waits
If a stream is created but stays empty for ~1 hour (for example, during a longwait.forToken() or wait.for()), the streams backend may garbage-collect it. When the run resumes and tries to use the stream, you’ll see S2Error: Stream is being deleted and the task retries from the beginning.
Two ways to avoid this:
- Close the stream before the wait and open a new one when the run resumes.
- Write a heartbeat record to the stream every 20–30 minutes during the wait so it’s never empty long enough to be deleted.

