# How to Implement HTTP Streaming and Server-Sent Events in Bun

> Learn to implement HTTP streaming and Server-Sent Events in Bun using native Web Streams integration. Effortlessly handle backpressure and cleanup with standard controllers.

- Repository: [Bun/bun](https://github.com/oven-sh/bun)
- Tags: how-to-guide
- Published: 2026-02-28

---

**Bun's native Web Streams integration allows you to implement HTTP streaming and Server-Sent Events by returning a `Response` with a `ReadableStream` body, using standard controllers for backpressure and a `cancel` hook for cleanup.**

The `oven-sh/bun` runtime treats streaming as a first-class citizen. Built around the Web Streams API, `Bun.serve` lets handlers return a `Response` whose body is any `ReadableStream`, enabling everything from chunked text delivery to high-performance binary streaming without external dependencies.

## Understanding Bun's Web Streams Architecture

Bun's HTTP server implementation in [`src/js/node/http2.ts`](https://github.com/oven-sh/bun/blob/main/src/js/node/http2.ts) and the `Server` class definitions in [`src/bun.js/api/server.classes.ts`](https://github.com/oven-sh/bun/blob/main/src/bun.js/api/server.classes.ts) expose the Web Streams API directly to user code. When a handler returns a `Response` wrapping a `ReadableStream`, Bun maps that stream to the underlying HTTP response sink, handling backpressure, chunked transfer encoding, and connection lifecycle automatically.

The stream controller exposes `enqueue()` for pushing data and `close()` for terminating the stream. For production use, you must implement the `cancel()` callback to release resources when clients disconnect prematurely.

## Implementing HTTP Streaming in Bun

### Basic ReadableStream Implementation

The standard pattern for HTTP streaming involves creating a `ReadableStream` with `start` and `pull` callbacks, then wrapping it in a `Response`. This example from the Bun test suite ([`test/js/bun/http/serve.test.ts`](https://github.com/oven-sh/bun/blob/main/test/js/bun/http/serve.test.ts), lines 525-560) demonstrates the fundamental approach:

```typescript
using server = Bun.serve({
  port: 0,
  async fetch(_req) {
    const data = "Hello, streaming world!\n";
    return new Response(
      new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode(data));
          controller.close();
        },
      })
    );
  },
});

console.log(`Server listening at ${server.url}`);

```

The `start` callback initializes the stream, `controller.enqueue()` accepts `Uint8Array`, `string`, or any `ArrayBufferView`, and `controller.close()` signals completion. Bun automatically selects chunked encoding when the content length is unknown.

### Handling Client Disconnections

Every streaming request must handle premature closure. Implement the `cancel` hook on the `ReadableStream` to release timers, file handles, or database connections. You can also monitor `req.signal` for abort events, as shown in [`serve.test.ts`](https://github.com/oven-sh/bun/blob/main/serve.test.ts) (lines 2860-2875):

```typescript
using server = Bun.serve({
  port: 0,
  async fetch(req) {
    const abortPromise = new Promise<void>((resolve) => {
      req.signal.addEventListener("abort", () => resolve());
    });

    return new Response(
      new ReadableStream({
        async start(controller) {
          for (let i = 0; i < 1_000; i++) {
            if (controller.desiredSize === 0) await Bun.sleep(10);
            controller.enqueue(`chunk ${i}\n`);
            if (await Promise.race([abortPromise, Promise.resolve()])) break;
          }
          controller.close();
        },
      })
    );
  },
});

```

The `cancel` method on the stream controller and the `req.signal` abort event both fire when the client disconnects, ensuring resources are not leaked.

## Building Server-Sent Events (SSE) Endpoints

### SSE Protocol Requirements

Server-Sent Events require specific HTTP headers and payload formatting. According to the Bun source implementation in [`serve.test.ts`](https://github.com/oven-sh/bun/blob/main/serve.test.ts) (lines 2001-2026), an SSE response must include:

- **`Content-Type: text/event-stream`** – identifies the response as an event stream
- **`Cache-Control: no-cache`** – prevents intermediaries from buffering events
- **Payload format** – messages prefixed with `data: ` and terminated by two newline characters (`\n\n`)

### Complete SSE Implementation

The following implementation demonstrates a robust SSE endpoint that sends periodic messages and cleans up resources when the client disconnects:

```typescript
using server = Bun.serve({
  port: 0,
  async fetch(_req) {
    let controller: ReadableStreamDefaultController | undefined;
    let interval: NodeJS.Timer | null = null;
    const payload = new TextEncoder().encode("data: hello\n\n");
    const CHUNKS = 5;

    interval = setInterval(() => {
      controller?.enqueue(payload);
      if (--CHUNKS === 0) {
        clearInterval(interval!);
        interval = null;
        controller?.close();
      }
    }, 1_000);

    return new Response(
      new ReadableStream({
        start(ctrl) { controller = ctrl; },
        cancel() { if (interval) clearInterval(interval); },
      }),
      {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
        },
      },
    );
  },
});

```

The `cancel` hook is essential here—it clears the interval timer when the client closes the connection, preventing memory leaks and unnecessary CPU usage.

### Cleanup and Resource Management

SSE connections are long-lived, making cleanup critical. Always implement the `cancel` callback on the `ReadableStream` to:

- Clear intervals or timeouts
- Close file descriptors or database connections
- Release memory buffers

The Bun runtime invokes `cancel` automatically when the underlying HTTP connection closes, whether through client disconnect, network error, or timeout.

## Optimizing with Direct Mode Streaming

For high-throughput scenarios involving large binary data or file transfers, Bun offers a **direct mode** that bypasses JavaScript-level buffering. In [`test/js/bun/http/serve-direct-readable-stream.test.ts`](https://github.com/oven-sh/bun/blob/main/test/js/bun/http/serve-direct-readable-stream.test.ts), the implementation shows how to use `type: "direct"`:

```typescript
using server = Bun.serve({
  port: 0,
  async fetch(_req) {
    return new Response(
      new ReadableStream({
        type: "direct",
        async pull(controller) {
          await controller.write("chunk");
          await Bun.sleep(0);
          controller.close();
        },
      })
    );
  },
});

```

In direct mode, the controller exposes `write`, `flush`, and `close` methods that operate on the underlying **HTTPResponseSink**. This provides zero-copy streaming ideal for:

- Serving large video files
- Proxying binary data from upstream services
- Real-time audio streaming

## Summary

- **Bun.serve** natively supports HTTP streaming by accepting a `ReadableStream` as a `Response` body, eliminating the need for external libraries.
- **Standard streaming** uses `controller.enqueue()` to push chunks and `controller.close()` to end the stream, with automatic backpressure handling.
- **Server-Sent Events** require `Content-Type: text/event-stream` headers and `data: …\n\n` payload formatting; always implement the `cancel` hook to prevent timer leaks.
- **Direct mode** (`type: "direct"`) offers zero-copy binary streaming via `controller.write()` for high-performance file and data delivery.
- **Cleanup** is mandatory for long-lived connections—use `cancel` callbacks and `req.signal` to release resources when clients disconnect.

## Frequently Asked Questions

### What is the difference between HTTP streaming and WebSockets in Bun?

HTTP streaming and Server-Sent Events use standard HTTP connections with a persistent response body, making them firewall-friendly and automatically handling reconnection via the browser's `EventSource` API. WebSockets require a protocol upgrade to `ws://` or `wss://` and provide bidirectional communication. In Bun, both are supported natively, but SSE is often simpler for unidirectional server-to-client updates like live logs or notifications.

### How do I handle client disconnections in Bun SSE streams?

Always implement the `cancel` callback on your `ReadableStream` and monitor `req.signal` for abort events. When a client closes the connection, Bun invokes `cancel`, allowing you to clear intervals, close file descriptors, or release database connections. Without this cleanup, long-lived SSE connections will leak memory and consume CPU cycles indefinitely.

### When should I use direct mode streaming versus standard ReadableStream?

Use **direct mode** (`type: "direct"`) when serving large binary files, proxying high-throughput data, or streaming video where zero-copy performance is critical. Direct mode writes directly to the socket via `controller.write()` and `controller.flush()`. Use the **standard `ReadableStream`** for text-based SSE, JSON streaming, or when you need automatic backpressure handling through `controller.enqueue()`.

### Does Bun support backpressure in HTTP streaming?

Yes, Bun automatically handles backpressure when using standard `ReadableStream` controllers. The `controller.desiredSize` property indicates when the internal queue is full, allowing you to pause data generation until the client catches up. In direct mode, you must manually manage flow control using `controller.flush()` to ensure data is sent immediately, though the underlying HTTP/2 implementation in [`src/js/node/http2.ts`](https://github.com/oven-sh/bun/blob/main/src/js/node/http2.ts) still manages TCP backpressure at the transport level.