OpenAI API Streaming Not Working? Fix SSE & Stream Errors
Diagnose and fix OpenAI streaming failures — ERR_STREAM_PREMATURE_CLOSE, SSE disconnects, null finish_reason, and infrastructure timeout mismatches — in Node.js and Python with working code examples.
What causes OpenAI streaming to break?
OpenAI streaming uses Server-Sent Events (SSE) over a chunked HTTP connection that stays open until the data: [DONE] sentinel arrives. Any intermediary that cuts this connection early — or a client that mishandles the stream lifecycle — produces one of the errors below.
- Infrastructure timeout mismatch: proxies, load balancers, or CDNs drop idle connections before the response finishes — appears at predictable 5- or 10-minute marks.
- Improper stream termination handling: clients that don't wait for
[DONE]or bail early on a nullfinish_reasontreat a valid completion as an error. - Client lifecycle issues: creating a new
OpenAI()instance per request exhausts connection pool resources and file descriptors. - Network instability: VPN interruptions or transient packet loss truncate the stream before the final chunk arrives.
- Missing retry logic: blind retry after partial delivery duplicates tokens already shown in the UI.
How to fix OpenAI streaming errors
Implement exponential backoff with jitter for streaming retries
Add retry logic that detects stream failures (ERR_STREAM_PREMATURE_CLOSE, fetch abort, timeout) and retries with exponential backoff. Generate a unique idempotency key per user request so a retry never duplicates tokens already displayed. Only retry on APIConnectionError or network-level errors — not on content errors like content_filter.
// Node.js / TypeScript
import OpenAI from 'openai';
import { v4 as uuidv4 } from 'uuid';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function streamWithRetry(messages, maxRetries = 3) {
const idempotencyKey = uuidv4();
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const stream = await client.chat.completions.create({
model: 'gpt-4o',
messages,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) process.stdout.write(content);
}
return; // success
} catch (error) {
lastError = error;
const retryable =
error instanceof OpenAI.APIConnectionError ||
error.code === 'ERR_STREAM_PREMATURE_CLOSE';
if (retryable && attempt < maxRetries - 1) {
const delayMs = Math.min(500 * Math.pow(2, attempt) + Math.random() * 500, 30000);
console.error(`Stream failed (attempt ${attempt + 1}), retrying in ${delayMs}ms`);
await new Promise(r => setTimeout(r, delayMs));
} else {
throw error;
}
}
}
throw lastError;
} Increase infrastructure timeouts to accommodate long-running streams
Configure all intermediary layers — load balancers, reverse proxies, CDNs, firewalls — with timeouts of at least 90 seconds (120+ recommended). If failures occur at exactly 5 minutes in Python or 10 minutes in Node.js, an intermediary timeout is almost certainly the cause. Also set the OpenAI client timeout explicitly on both platforms.
// Node.js: explicit client timeout
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
timeout: 90_000, // 90 seconds in milliseconds
}); # Python: explicit timeout via httpx
import httpx
from openai import OpenAI
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
timeout=httpx.Timeout(90.0),
) # Nginx — add to the location block that proxies to your app
proxy_read_timeout 120s;
proxy_connect_timeout 30s;
proxy_send_timeout 30s; Use a single persistent OpenAI client instance
Each new OpenAI() call creates a new internal connection pool. Creating a client per request or per route handler exhausts system file descriptors and connection limits under load. Create one client at module level and reuse it across your entire application.
// lib/openai-client.ts — create once, import everywhere
import OpenAI from 'openai';
export const openaiClient = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
timeout: 90_000,
maxRetries: 2,
});
// In route handlers:
// import { openaiClient } from './lib/openai-client'; # openai_client.py — module-level singleton
import os
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# In handlers:
# from openai_client import client Handle null or missing finish_reason correctly
finish_reason may be null until the final chunk, and may be absent entirely when content filters are active or max_tokens is exceeded. Never treat a missing finish_reason as a fatal error. Detect stream completion via the [DONE] sentinel or a clean connection close, and log the null case with context for diagnostics.
// Node.js: safe finish_reason handling
async function streamChatCompletion(messages) {
const client = new OpenAI();
let fullResponse = '';
let finishReason = null;
const stream = await client.chat.completions.create({
model: 'gpt-4o',
messages,
stream: true,
});
for await (const event of stream) {
const choice = event.choices[0];
if (choice.delta?.content) {
fullResponse += choice.delta.content;
process.stdout.write(choice.delta.content);
}
// finish_reason is null on all but the last chunk
if (choice.finish_reason) finishReason = choice.finish_reason;
}
if (!finishReason) {
// Not fatal — log for diagnostics and continue
console.warn('Stream completed without finish_reason', {
responseLength: fullResponse.length,
timestamp: new Date().toISOString(),
});
finishReason = 'unknown';
}
return { fullResponse, finishReason };
} Monitor stream health and set up error tracking
Track stream duration, chunk latency, error rate, and timeout frequency. Set alerts for elevated rates of ERR_STREAM_PREMATURE_CLOSE. Check prismix.dev/service/openai to determine whether a spike is API-wide or isolated to your infrastructure.
// Minimal stream monitoring wrapper
async function streamWithMonitoring(messages) {
const start = Date.now();
let chunkCount = 0;
let totalBytes = 0;
try {
const stream = await client.chat.completions.create({
model: 'gpt-4o', messages, stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
chunkCount++;
totalBytes += Buffer.byteLength(content);
process.stdout.write(content);
}
console.log('Stream OK', { ms: Date.now() - start, chunkCount, totalBytes });
} catch (error) {
console.error('Stream failed', {
code: error.code,
ms: Date.now() - start,
chunkCount,
totalBytes,
});
throw error;
}
} Get an email the next time OpenAI goes down
Outage alerts for OpenAI, straight to your inbox. No account needed, unsubscribe in every email.
Watching more than one service? A free account covers 5 services + a daily digest — and Pro is currently free.
FAQ
Why does my stream suddenly stop mid-response with ERR_STREAM_PREMATURE_CLOSE?
This error means the HTTP connection was closed before the server finished streaming. The most common culprit is an infrastructure timeout — increase your load balancer or proxy read_timeout to at least 90 seconds. If the error is sporadic rather than time-based, it is more likely network instability or a transient OpenAI issue. Check prismix.dev/service/openai to rule out an API-wide incident.
My streaming works for short responses but fails for long ones (30+ seconds). What's wrong?
Long responses hit timeout thresholds set below the generation time. If failures occur at exactly 5 minutes in Python or 10 minutes in Node.js, an intermediary — Nginx, an AWS ALB, Cloudflare — is dropping the connection. Increase proxy_read_timeout in Nginx to 120+ seconds, set the OpenAI client timeout to at least 90 seconds, and test with a long-form prompt like “Write a 500-word essay.”
The last chunk arrives without a finish_reason. Is this an error?
No. A missing finish_reason in the final chunk is not fatal. It can occur when content filters trigger, max_tokens is exceeded, or due to a transient provider inconsistency. Listen for the stream closing cleanly rather than relying on finish_reason. Log the occurrence with request metadata and treat the response as complete.
Can I safely retry a failed stream without duplicating tokens in the UI?
Yes, with idempotency tracking. Generate a UUID for each user request and store it alongside tokens already displayed. On retry, pass the same key so the server (or your deduplication layer) can detect duplicates. Never blind-retry after a partial delivery — that guarantees duplicated tokens. Use exponential backoff with a 0.5-second base and a 30-second cap, and give up after 3 attempts. Only retry on APIConnectionError or ERR_STREAM_PREMATURE_CLOSE, not on content errors.