Skip to main content
doc0blade47/honodocumentation home
Sign in

Streaming and SSE

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Hono provides robust streaming primitives and specialized helpers for real-time data delivery, chunked data pipelines, and server-sent events across modern web runtimes. By leveraging underlying Web Standard TransformStream and ReadableStream primitives, these APIs manage low-level connection lifecycles, backpressure, and abort signals seamlessly without blocking execution.

Sources: src/utils/stream.ts:6-46, src/helper/streaming/sse.ts:72-99, src/helper/streaming/stream.ts:7-13

Core Streaming Architecture and Backpressure

Plumbing and State Management

Hono's streaming infrastructure centers around the StreamingApi class, which bridges Web Standard TransformStream, WritableStream, and ReadableStream instances to manage chunk delivery and backpressure.

The StreamingApi constructor accepts both a writable and an internal _readable stream, immediately acquiring a default writer via writable.getWriter() and a reader via _readable.getReader(). It exposes a public responseReadable ReadableStream whose pull() controller reads chunks from the underlying reader, propagating backpressure signals naturally from the consumer to the producer.

export class StreamingApi {
  private writer: WritableStreamDefaultWriter<Uint8Array>
  private encoder: TextEncoder
  private writable: WritableStream
  private abortSubscribers: (() => void | Promise<void>)[] = []
  responseReadable: ReadableStream
  /**
   * Whether the stream has been aborted.
   */
  aborted: boolean = false
  /**
   * Whether the stream has been closed normally.
   */
  closed: boolean = false
 
  constructor(writable: WritableStream, _readable: ReadableStream) {
    this.writable = writable
    this.writer = writable.getWriter()
    this.encoder = new TextEncoder()
 
    const reader = _readable.getReader()
 
    // in case the user disconnects, let the reader know to cancel
    // this in-turn results in responseReadable being closed
    // and writeSSE method no longer blocks indefinitely
    this.abortSubscribers.push(async () => {
      await reader.cancel()
    })
 
    this.responseReadable = new ReadableStream({
      async pull(controller) {
        const { done, value } = await reader.read()
        done ? controller.close() : controller.enqueue(value)
      },
      cancel: () => {
        if (!this.closed) {
          this.abort()
        }
      },
    })
  }

Sources: src/utils/stream.ts:6-46

Note

When piping an external ReadableStream into StreamingApi via pipe(body), the internal writer lock is released using writer.releaseLock() before calling body.pipeTo(this.writable, { preventClose: true }), and a new writer is acquired afterward.

async pipe(body: ReadableStream) {
  this.writer.releaseLock()
  await body.pipeTo(this.writable, { preventClose: true })
  this.writer = this.writable.getWriter()
}

Sources: src/utils/stream.ts:78-82

Abort Signals and Runtimes

Connection aborts and cancellation requests trigger subscribers registered in abortSubscribers. When responseReadable is canceled or an external abort occurs, StreamingApi.abort() iterates through all subscribers, invoking functions such as reader.cancel() to unblock pending writes and close the pipeline.

onAbort(listener: () => void | Promise<void>) {
  this.abortSubscribers.push(listener)
}
 
/**
 * Abort the stream.
 * You can call this method when stream is aborted by external event.
 */
abort() {
  if (!this.aborted) {
    this.aborted = true
    this.abortSubscribers.forEach((subscriber) => subscriber())
  }
}

Sources: src/utils/stream.ts:84-97

Warning

Versions of Bun prior to v1.1.27 do not automatically call cancel() on ReadableStream objects returned by Bun.serve(). Hono works around this by listening to request signal abort events on older runtimes to manually invoke stream.abort().

// Until Bun v1.1.27, Bun didn't call cancel() on the ReadableStream for Response objects from Bun.serve()
if (isOldBunVersion()) {
  c.req.raw.signal.addEventListener('abort', () => {
    if (!stream.closed) {
      stream.abort()
    }
  })
}

Sources: src/helper/streaming/stream.ts:15-22

Binary and Chunked Stream Helper

Public Stream API and Response Creation

The public stream helper function initializes a chunked response workflow by constructing a TransformStream, instantiating a StreamingApi instance, and returning a response via c.newResponse(stream.responseReadable). It accepts a callback function cb receiving the active StreamingApi and an optional onError handler.

export const stream = (
  c: Context,
  cb: (stream: StreamingApi) => Promise<void>,
  onError?: (e: Error, stream: StreamingApi) => Promise<void>
): Response => {
  const { readable, writable } = new TransformStream()
  const stream = new StreamingApi(writable, readable)

Sources: src/helper/streaming/stream.ts:7-13, src/helper/streaming/stream.ts:44-44

Note

In runtimes such as Bun, the context object c is destroyed immediately when the initial response is returned. To prevent premature garbage collection, Hono stores the context reference in a WeakMap (contextStash) keyed by stream.responseReadable until streaming completes.

// in bun, `c` is destroyed when the request is returned, so hold it until the end of streaming
contextStash.set(stream.responseReadable, c)
;(async () => {

Sources: src/helper/streaming/stream.ts:24-26

Raw Chunk Writes and Stream Control

The StreamingApi class exposes methods for transmitting raw binary and text chunks, inserting newlines, delaying execution, piping streams, and closing connections.

MethodParametersReturn TypeDescription
writeinput: Uint8Array | stringPromise<StreamingApi>Encodes string inputs via TextEncoder or writes raw Uint8Array bytes to the underlying writer.
writelninput: stringPromise<StreamingApi>Appends a newline character (\n) to the input string and writes it.
sleepms: numberPromise<unknown>Pauses execution for the specified milliseconds using setTimeout.
pipebody: ReadableStreamPromise<void>Releases the writer lock, pipes an external readable stream into the writable target, and re-acquires the writer.
closenonePromise<void>Marks the stream as closed and closes the underlying writer.
onAbortlistener: () => void | Promise<void>voidRegisters a callback invoked when the stream aborts.
abortnonevoidSets the aborted flag to true and triggers all registered abort subscribers.
async write(input: Uint8Array | string): Promise<StreamingApi> {
  try {
    if (typeof input === 'string') {
      input = this.encoder.encode(input)
    }
    await this.writer.write(input)
  } catch {
    // Do nothing. If you want to handle errors, create a stream by yourself.
  }
  return this
}
 
async writeln(input: string): Promise<StreamingApi> {
  await this.write(input + '\n')
  return this
}
 
sleep(ms: number): Promise<unknown> {
  return new Promise((res) => setTimeout(res, ms))
}
 
async close() {
  this.closed = true
  try {
    await this.writer.close()
  } catch {
    // Do nothing. If you want to handle errors, create a stream by yourself.
  }
}
 
async pipe(body: ReadableStream) {
  this.writer.releaseLock()
  await body.pipeTo(this.writable, { preventClose: true })
  this.writer = this.writable.getWriter()
}
 
onAbort(listener: () => void | Promise<void>) {
  this.abortSubscribers.push(listener)
}
 
/**
 * Abort the stream.
 * You can call this method when stream is aborted by external event.
 */
abort() {
  if (!this.aborted) {
    this.aborted = true
    this.abortSubscribers.forEach((subscriber) => subscriber())
  }
}

Sources: src/utils/stream.ts:48-97

Warning

Both write and close catch and suppress write errors internally. Developers requiring custom error propagation or handling during raw chunk transmission must construct and manage custom streams directly rather than relying on the helper defaults.

try {
  if (typeof input === 'string') {
    input = this.encoder.encode(input)
  }
  await this.writer.write(input)
} catch {
  // Do nothing. If you want to handle errors, create a stream by yourself.
}

Sources: src/utils/stream.ts:49-56, src/utils/stream.ts:71-76

Plaintext Streaming Helper

Configuration and Semantics

The streamText helper function sets up a response specifically optimized for plain text streaming. It configures the response headers by setting Content-Type to text/plain, X-Content-Type-Options to nosniff, and Transfer-Encoding to chunked before delegating the underlying stream creation to the base stream function.

export const streamText = (
  c: Context,
  cb: (stream: StreamingApi) => Promise<void>,
  onError?: (e: Error, stream: StreamingApi) => Promise<void>
): Response => {
  c.header('Content-Type', TEXT_PLAIN)
  c.header('X-Content-Type-Options', 'nosniff')
  c.header('Transfer-Encoding', 'chunked')
  return stream(c, cb, onError)
}

Sources: src/helper/streaming/text.ts:6-15

Header NameValuePurpose
Content-Typetext/plainInstructs the client to interpret the incoming payload as plaintext content.
X-Content-Type-OptionsnosniffPrevents MIME-type sniffing by enforcing strict adherence to the declared content type.
Transfer-EncodingchunkedEnables chunked transfer encoding for streaming responses without a predefined content length.
c.header('Content-Type', TEXT_PLAIN)
c.header('X-Content-Type-Options', 'nosniff')
c.header('Transfer-Encoding', 'chunked')

Sources: src/helper/streaming/text.ts:11-13

Note

The streamText function accepts three parameters: the Hono Context object c, a mandatory callback function cb receiving the active StreamingApi instance, and an optional onError error-handling callback.

export const streamText = (
  c: Context,
  cb: (stream: StreamingApi) => Promise<void>,
  onError?: (e: Error, stream: StreamingApi) => Promise<void>
): Response => {

Sources: src/helper/streaming/text.ts:6-10

Server-Sent Events API and Lifecycle

SSEStreamingApi and Connection Setup

The SSEStreamingApi class extends StreamingApi to provide specialized formatting for Server-Sent Events messages via writeSSE. The streamSSE helper initializes this architecture by creating a TransformStream pair, instantiating SSEStreamingApi with the writable and readable ends, and establishing essential response headers for event-stream communication.

Header NameValuePurpose
Transfer-EncodingchunkedEnables chunked transfer encoding for continuous streaming payloads.
Content-Typetext/event-streamInstructs the client to parse the incoming stream as Server-Sent Events.
Cache-Controlno-cachePrevents intermediaries and clients from caching the event stream.
Connectionkeep-aliveKeeps the underlying TCP connection open for continuous event transmission.

Sources: src/helper/streaming/sse.ts:13-16, src/helper/streaming/sse.ts:72-96

Note

In runtime environments such as Bun, context objects can be destroyed immediately after the initial response returns. To prevent premature garbage collection, Hono stores the context reference inside a contextStash WeakMap keyed by stream.responseReadable until the stream lifecycle completes.

const contextStash: WeakMap<ReadableStream, Context> = new WeakMap<ReadableStream, Context>()

Sources: src/helper/streaming/sse.ts:70-70, src/helper/streaming/sse.ts:89-91

Warning

Prior to Bun v1.1.27, runtime responses from Bun.serve() did not automatically invoke cancel() on the underlying ReadableStream. Hono mitigates this by manually listening to request abort signals on older Bun versions and triggering stream.abort() if the stream remains unclosed.

// Until Bun v1.1.27, Bun didn't call cancel() on the ReadableStream for Response objects from Bun.serve()
if (isOldBunVersion()) {
  c.req.raw.signal.addEventListener('abort', () => {
    if (!stream.closed) {
      stream.abort()
    }
  })
}

Sources: src/helper/streaming/sse.ts:80-87

Execution Walkthrough and Sequence

The execution lifecycle of a Server-Sent Events stream follows a strict invocation path managed by the run helper function, executing the user callback, handling errors, and ensuring resource cleanup.

Sources: src/helper/streaming/sse.ts:47-68, src/helper/streaming/sse.ts:97-97

The lifecycle operations proceed through the following steps:

  1. streamSSE invokes run passing the active SSEStreamingApi instance, callback, and optional error handler. Sources: src/helper/streaming/sse.ts:97-97
  2. run awaits the user callback cb(stream). If an error occurs and onError is provided, it executes onError(e, stream) followed by writing an error event to the stream; otherwise, errors are logged to console.error. Sources: src/helper/streaming/sse.ts:52-64
  3. close is guaranteed to execute in the finally block, marking this.closed = true and closing the underlying writer to finalize the transmission. Sources: src/helper/streaming/sse.ts:65-67, src/utils/stream.ts:69-76

Event Framing and Serialization

Execution Walkthrough

The serialization of Server-Sent Events messages is handled by the writeSSE method on the SSEStreamingApi class. This method processes incoming message payloads, validates field properties for illegal line breaks, formats them into the standard SSE wire format, and dispatches the raw bytes down the stream via streamSSE → run → writeSSE → write.

Sources: src/helper/streaming/sse.ts:18-44, src/helper/streaming/sse.ts:47-68, src/helper/streaming/sse.ts:97-97, src/utils/stream.ts:48-58

  1. streamSSE initializes the transformation stream and invokes run. Sources: src/helper/streaming/sse.ts:72-97
  2. run executes the user-supplied callback function. Sources: src/helper/streaming/sse.ts:47-53
  3. writeSSE receives an SSEMessage object, resolving any asynchronous data promises and formatting fields into text blocks. Sources: src/helper/streaming/sse.ts:18-41
  4. write encodes the formatted string into a Uint8Array via TextEncoder and writes it to the underlying writable stream writer. Sources: src/helper/streaming/sse.ts:43-43, src/utils/stream.ts:7-24, src/utils/stream.ts:48-58

Message Structure and Validation

The SSEMessage interface supports four specific fields used to frame events. Data payloads can be provided as a string or a Promise<string>.

FieldTypeDescription
datastring | Promise<string>The payload content of the message; multi-line strings are automatically prefixed with data: on each line.
eventstring (optional)The event type identifier for the message.
idstring (optional)The event stream synchronization ID.
retrynumber (optional)The reconnection time interval in milliseconds.

Sources: src/helper/streaming/sse.ts:6-11, src/helper/streaming/sse.ts:18-41

Caution

Control character injection is strictly guarded during message serialization. If any of the tracked meta-fields (event, id, or retry) contain carriage return (\r) or newline (\n) characters, writeSSE immediately throws an error to prevent protocol corruption.

for (const key of ['event', 'id', 'retry'] as (keyof SSEMessage)[]) {
  if (message[key] && /[\r\n]/.test(message[key] as string)) {
    throw new Error(`${key} must not contain "\\r" or "\\n"`)
  }
}

Sources: src/helper/streaming/sse.ts:27-31

Was this page helpful?