DEV Community

Preecha
Preecha

Posted on

How Do You Stream API Responses Using Server-Sent Events (SSE)?

TL;DR

Use Server-Sent Events (SSE) to stream API responses over HTTP. Send Content-Type: text/event-stream and write events as data: {json}\n\n. SSE works well for AI response streaming, progress updates, and live feeds. Modern PetstoreAPI uses SSE for AI pet recommendations and order status updates.

Try Apidog today

Introduction

Your API generates AI recommendations for pets, but the response takes 10 seconds. Do you make users wait, or stream results as they’re generated?

With Server-Sent Events (SSE), you can stream responses in real time. Users see results immediately as the AI generates them, creating a better experience.

Modern PetstoreAPI uses SSE for:

  • AI pet recommendations
  • Order status updates
  • Inventory changes

If you’re testing streaming APIs, Apidog supports SSE testing and validation.

SSE Basics

SSE is an HTTP-based, one-way stream from a server to a client.

The client opens a long-lived HTTP connection. The server sends events as text, and the client processes each event as it arrives.

SSE Message Format

An SSE response should include these headers:

Content-Type: text/event-stream
Cache-Control: no-cache
Enter fullscreen mode Exit fullscreen mode

Each event contains a data: field and ends with two newline characters:

data: {"message":"First chunk"}

data: {"message":"Second chunk"}

data: {"message":"Third chunk"}

Enter fullscreen mode Exit fullscreen mode

Each event:

  • Starts with data:
  • Contains a text payload
  • Ends with \n\n

When sending JSON, serialize the object before writing it to the response.

Named Events

Use the event: field to distinguish different event types:

event: recommendation
data: {"petId":"019b4132","score":0.95}

event: recommendation
data: {"petId":"019b4127","score":0.89}

event: complete
data: {"total":2}

Enter fullscreen mode Exit fullscreen mode

Clients can register separate handlers for recommendation and complete.

Event IDs

Add an id: field when clients need to resume after a disconnection:

id: 1
data: {"message":"First"}

id: 2
data: {"message":"Second"}

Enter fullscreen mode Exit fullscreen mode

The browser can send the last received event ID in the Last-Event-ID request header when reconnecting. The server can use that value to resume the stream.

Implementing an SSE Server

Node.js and Express

The following endpoint streams recommendations and then sends a named completion event:

app.get('/v1/pets/recommendations/stream', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const recommendations = await generateRecommendations(req.query.userId);

  for (const recommendation of recommendations) {
    res.write(`data: ${JSON.stringify(recommendation)}\n\n`);

    // Simulate a delay between streamed results.
    await sleep(100);
  }

  res.write(
    `event: complete\ndata: ${JSON.stringify({
      total: recommendations.length
    })}\n\n`
  );

  res.end();
});
Enter fullscreen mode Exit fullscreen mode

For production deployments behind Nginx, also disable response buffering:

res.setHeader('X-Accel-Buffering', 'no');
Enter fullscreen mode Exit fullscreen mode

Python and FastAPI

FastAPI can return an async generator through StreamingResponse:

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()

@app.get("/v1/pets/recommendations/stream")
async def stream_recommendations(user_id: str):
    async def generate():
        recommendations = await get_recommendations(user_id)

        for recommendation in recommendations:
            yield f"data: {json.dumps(recommendation)}\n\n"
            await asyncio.sleep(0.1)

        complete_event = {
            "total": len(recommendations)
        }

        yield (
            "event: complete\n"
            f"data: {json.dumps(complete_event)}\n\n"
        )

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive"
        }
    )
Enter fullscreen mode Exit fullscreen mode

Implementing an SSE Client

JavaScript in the Browser

The browser provides the EventSource API for SSE connections:

const eventSource = new EventSource(
  'https://petstoreapi.com/v1/pets/recommendations/stream?userId=user-456'
);

eventSource.onmessage = (event) => {
  const recommendation = JSON.parse(event.data);
  displayRecommendation(recommendation);
};

eventSource.addEventListener('complete', (event) => {
  const result = JSON.parse(event.data);

  console.log(`Received ${result.total} recommendations`);
  eventSource.close();
});

eventSource.onerror = (error) => {
  console.error('SSE error:', error);
};
Enter fullscreen mode Exit fullscreen mode

onmessage handles events without a custom event name. Use addEventListener() for named events such as complete or recommendation.

By default, EventSource attempts to reconnect when the connection fails. Close it explicitly when the stream is complete or when your application no longer needs the connection.

React Hook

A custom React hook can collect streamed items and track completion:

import { useEffect, useState } from 'react';

function useSSE(url) {
  const [data, setData] = useState([]);
  const [complete, setComplete] = useState(false);

  useEffect(() => {
    const eventSource = new EventSource(url);

    eventSource.onmessage = (event) => {
      const item = JSON.parse(event.data);
      setData((previous) => [...previous, item]);
    };

    eventSource.addEventListener('complete', () => {
      setComplete(true);
      eventSource.close();
    });

    return () => {
      eventSource.close();
    };
  }, [url]);

  return { data, complete };
}
Enter fullscreen mode Exit fullscreen mode

Use the hook in a component:

function Recommendations({ userId }) {
  const { data, complete } = useSSE(
    `https://petstoreapi.com/v1/pets/recommendations/stream?userId=${userId}`
  );

  return (
    <div>
      {data.map((recommendation) => (
        <PetCard
          key={recommendation.petId}
          pet={recommendation}
        />
      ))}

      {!complete && <Spinner />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

How Modern PetstoreAPI Uses SSE

AI Pet Recommendations

Stream AI-generated recommendations as they become available:

GET /v1/pets/recommendations/stream?userId=user-456
Accept: text/event-stream
Enter fullscreen mode Exit fullscreen mode

Example response:

event: recommendation
data: {"petId":"019b4132","name":"Fluffy","score":0.95,"reason":"Matches your preference for cats"}

event: recommendation
data: {"petId":"019b4127","name":"Buddy","score":0.89,"reason":"Similar to pets you liked"}

event: complete
data: {"total":2,"processingTime":850}

Enter fullscreen mode Exit fullscreen mode

Order Status Updates

Stream order processing steps:

GET /v1/orders/019b4132/status/stream
Accept: text/event-stream
Enter fullscreen mode Exit fullscreen mode

Example response:

data: {"status":"payment_processing","timestamp":"2026-03-13T10:30:00Z"}

data: {"status":"payment_confirmed","timestamp":"2026-03-13T10:30:02Z"}

data: {"status":"preparing_shipment","timestamp":"2026-03-13T10:30:05Z"}

event: complete
data: {"status":"shipped","trackingNumber":"1Z999AA10123456784"}

Enter fullscreen mode Exit fullscreen mode

Inventory Changes

Stream real-time inventory updates:

GET /v1/inventory/stream
Accept: text/event-stream
Enter fullscreen mode Exit fullscreen mode

Example response:

event: stock-change
data: {"petId":"019b4132","oldStock":5,"newStock":4}

event: price-change
data: {"petId":"019b4127","oldPrice":299.99,"newPrice":279.99}

Enter fullscreen mode Exit fullscreen mode

See Modern PetstoreAPI SSE docs.

Testing SSE with Apidog

Use Apidog to test and validate an SSE endpoint:

  1. Create an SSE request.
  2. Set the Accept header to text/event-stream.
  3. Connect to the endpoint.
  4. View events as they arrive.
  5. Validate event names and payload formats.
  6. Test reconnection behavior.

Best Practices

1. Set the Required Headers

Set the response headers before writing any event data:

res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Disable Nginx buffering
Enter fullscreen mode Exit fullscreen mode

The X-Accel-Buffering header is useful when the response passes through Nginx.

2. Send Heartbeats

Long-lived connections can be closed by proxies or load balancers when no data is sent. Send comment lines as heartbeats:

const heartbeat = setInterval(() => {
  res.write(': heartbeat\n\n');
}, 15000);

res.on('close', () => {
  clearInterval(heartbeat);
});
Enter fullscreen mode Exit fullscreen mode

A line beginning with : is ignored by the SSE client but keeps the connection active.

3. Handle Errors Gracefully

Handle errors on the client and decide whether to keep the connection open for automatic reconnection or close it:

eventSource.onerror = (error) => {
  console.error('SSE error:', error);

  if (eventSource.readyState === EventSource.CLOSED) {
    console.error('SSE connection closed');
  }
};
Enter fullscreen mode Exit fullscreen mode

On the server, clean up resources when the client disconnects:

res.on('close', () => {
  // Cancel work, clear timers, and release resources.
});
Enter fullscreen mode Exit fullscreen mode

4. Use Event IDs for Resuming

Assign IDs to events and read the Last-Event-ID header when a client reconnects:

app.get('/stream', (req, res) => {
  const lastEventId = Number.parseInt(
    req.headers['last-event-id'] || '0',
    10
  );

  for (let id = lastEventId + 1; id <= 100; id++) {
    res.write(
      `id: ${id}\n` +
      `data: ${JSON.stringify({ message: `Event ${id}` })}\n\n`
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

The server should retain enough event history to replay events that the client may have missed.

5. Close Connections Explicitly

Close the client connection after a terminal event:

eventSource.addEventListener('complete', () => {
  eventSource.close();
});
Enter fullscreen mode Exit fullscreen mode

Also clean up server-side resources when the response closes:

res.on('close', () => {
  // Cleanup resources.
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

SSE is useful for streaming API responses when communication only needs to travel from the server to the client. It works over HTTP, is simpler than WebSocket for one-way communication, and supports automatic client reconnection.

Modern PetstoreAPI uses SSE for AI streaming, order updates, and live feeds. Test SSE endpoints with Apidog to validate event formats, payloads, and reconnection behavior.

FAQ

Can SSE work through corporate firewalls?

Yes. SSE uses standard HTTP or HTTPS, so it works through most firewalls and proxies.

How long can SSE connections stay open?

They can stay open indefinitely, but send heartbeats every 15–30 seconds to help keep connections alive through proxies.

Can I send binary data over SSE?

No. SSE is text-only. Base64-encode binary data or use WebSocket instead.

Does SSE support bidirectional communication?

No. SSE is server-to-client only. Clients must use regular HTTP requests or another protocol for client-to-server communication.

How many SSE connections can a browser have?

Browsers limit SSE connections per domain, typically to six connections. Use multiplexing or WebSocket when an application needs many concurrent streams.

Top comments (0)