DEV Community

Cover image for How Do You Stream API Responses Using Server-Sent Events (SSE)?
Wanda
Wanda

Posted on • Originally published at apidog.com

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

TL;DR

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

Try Apidog today

Introduction

If your API generates AI recommendations for pets and the response takes 10 seconds, don't make users wait for the full response. Stream results as they're generated for a real-time experience using Server-Sent Events (SSE).

SSE enables you to stream results to the user as soon as they're available, improving perceived responsiveness.

Modern PetstoreAPI leverages SSE for AI recommendations, order status, and inventory updates.

If you need to test streaming APIs, Apidog supports SSE testing and validation.

SSE Basics

SSE provides HTTP-based, one-way streaming from server to client.

SSE Format

Content-Type: text/event-stream
Cache-Control: no-cache

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

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

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

Enter fullscreen mode Exit fullscreen mode

Each event:

  • Begins with data:
  • Contains the payload
  • Ends with two newlines (\n\n)

Named Events

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

Event IDs

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

id: 2
data: {"message":"Second"}
Enter fullscreen mode Exit fullscreen mode

Clients can use the last event ID to resume a disconnected stream.

Implementing SSE Server

Node.js/Express Example

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

  // Generate and stream recommendations
  const recommendations = await generateRecommendations(req.query.userId);

  for (const rec of recommendations) {
    res.write(`data: ${JSON.stringify(rec)}\n\n`);
    await sleep(100); // Simulate delay
  }

  // Signal completion
  res.write(`event: complete\ndata: {"total":${recommendations.length}}\n\n`);
  res.end();
});
Enter fullscreen mode Exit fullscreen mode

Python/FastAPI Example

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

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 rec in recommendations:
            yield f"data: {json.dumps(rec)}\n\n"
            await asyncio.sleep(0.1)

        yield f"event: complete\ndata: {json.dumps({'total': len(recommendations)})}\n\n"

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

SSE Client Implementation

JavaScript/Browser

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

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

eventSource.addEventListener('complete', (event) => {
  const data = JSON.parse(event.data);
  console.log(`Received ${data.total} recommendations`);
  eventSource.close();
});

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

React Hook

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(prev => [...prev, item]);
    };

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

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

  return { data, complete };
}

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

  return (
    <div>
      {data.map(rec => (
        <PetCard key={rec.petId} pet={rec} />
      ))}
      {!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

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 events step-by-step:

GET /v1/orders/019b4132/status/stream
Accept: text/event-stream

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 inventory updates in real-time:

GET /v1/inventory/stream
Accept: text/event-stream

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

Apidog allows you to:

  1. Create SSE requests
  2. Set Accept: text/event-stream
  3. Connect and view events in real-time
  4. Validate event format
  5. Test reconnection logic

Best Practices

1. Set Proper Headers

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

2. Send Heartbeats

Keep the connection alive with periodic heartbeats.

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

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

3. Handle Errors Gracefully

eventSource.onerror = (error) => {
  if (eventSource.readyState === EventSource.CLOSED) {
    // Connection closed, will auto-reconnect
  } else {
    // Handle other errors
    console.error('SSE error:', error);
  }
};
Enter fullscreen mode Exit fullscreen mode

4. Use Event IDs for Resume

Let clients resume streams on reconnect.

let lastEventId = 0;

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

  for (let i = startId + 1; i <= 100; i++) {
    res.write(`id: ${i}\ndata: {"message":"Event ${i}"}\n\n`);
  }
});
Enter fullscreen mode Exit fullscreen mode

5. Close Connections

Properly close connections on completion:

// Client
eventSource.addEventListener('complete', () => {
  eventSource.close();
});

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

Conclusion

SSE is a practical choice for streaming API responses, particularly for one-way communication. It operates over HTTP and automatically handles reconnections.

Modern PetstoreAPI uses SSE for AI streaming, order status, and live feeds. Test your SSE endpoints with Apidog.

FAQ

Can SSE work through corporate firewalls?

Yes, SSE uses standard HTTP/HTTPS and works through most firewalls and proxies.

How long can SSE connections stay open?

Indefinitely. Send heartbeats every 15-30 seconds to keep connections alive through proxies.

Can I send binary data over SSE?

No, SSE is text-only. Base64-encode binary data or use WebSocket for binary messaging.

Does SSE support bidirectional communication?

No, SSE streams data from server to client only. Use regular HTTP requests for client-to-server communication.

How many SSE connections can a browser have?

Browsers typically limit SSE connections per domain (often 6). Use multiplexing or switch to WebSocket for many simultaneous connections.

Top comments (0)