AI Prompt Engineering

Getting ChatGPT to Write Accurate SSE Handlers Without Dropped Events

July 14, 2026 9 min read

You ask ChatGPT for a Server-Sent Events handler, and it returns something that looks perfectly reasonable. It compiles, the browser connects, and events flow β€” right up until the client drops and reconnects, at which point everything your server sent during the outage is gone. Silently. No error, no warning, just missing data.

This is the specific failure mode SSE code from ChatGPT reliably produces unless you prompt around it. The model knows the SSE format, but it defaults to the simplest possible implementation: no event IDs, no replay buffer, no heartbeat. That's fine for a demo; it's broken in production.

What You'll Learn

  • Why the default ChatGPT SSE output silently drops events on reconnect
  • How to prompt for correct Last-Event-ID handling and server-side replay
  • How to get ChatGPT to generate proper flush and buffering behavior
  • How to prompt for heartbeat logic that keeps connections alive through proxies
  • The specific prompt patterns that produce production-grade SSE code

Prerequisites

You should be comfortable with HTTP fundamentals and have written at least one server-side route handler. Examples here use Node.js with Express, but the prompting patterns transfer directly to Python (FastAPI/Flask) or Go. You'll need a ChatGPT account β€” GPT-4o is strongly preferred over GPT-3.5 for this kind of systems code.

How SSE Reconnection Actually Works

Before you can prompt ChatGPT correctly, you need to understand what correct actually means. The SSE spec (WHATWG) defines a reconnection protocol built around event IDs. Every event the server sends can carry an id: field. The browser stores the last seen ID and, on reconnect, sends it back in the Last-Event-ID HTTP header.

The server is responsible for reading that header and replaying any events the client missed. If the server ignores Last-Event-ID, every reconnect starts from scratch. For a live dashboard that's updating continuously, this means the client can silently miss minutes of state changes.

The other piece is flushing. SSE depends on chunked transfer encoding: each event must be flushed to the socket immediately. If your framework or a proxy is buffering the response, events queue up and arrive in bursts or not at all. Nginx, for example, will buffer SSE responses unless you explicitly disable proxy buffering for that route.

The Default ChatGPT Output and Where It Fails

Ask ChatGPT "Write an SSE endpoint in Express" and you'll get something close to this:

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

  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);

  req.on('close', () => clearInterval(interval));
});

This is functional for a local demo. But count what's missing: no id: field on each event, no Last-Event-ID header check, no replay logic, no res.flush() call (critical if you're using compression middleware), and no heartbeat to prevent proxies from dropping idle connections. If the client disconnects for two seconds and reconnects, it sees nothing from that gap.

This is the same pattern described in getting ChatGPT to write accurate event-driven architecture configs: the model produces code that passes the happy path and fails on edge cases it wasn't told to handle.

Prompting for Event ID Tracking and Replay

The fix starts with being explicit about the reconnection contract in your prompt. ChatGPT won't add replay logic unless you tell it the replay requirement exists.

Prompt pattern: "Write an Express SSE endpoint that assigns a monotonically incrementing integer id to every event. On each new connection, read the Last-Event-ID header. If it's present, replay all events with an ID greater than that value from an in-memory circular buffer of the last 200 events. After replay, continue sending new events in real time."

With that prompt, ChatGPT produces something much closer to this:

const express = require('express');
const app = express();

const EVENT_BUFFER_SIZE = 200;
const eventBuffer = [];
let eventCounter = 0;

function pushEvent(data) {
  eventCounter++;
  const event = { id: eventCounter, data };
  eventBuffer.push(event);
  if (eventBuffer.length > EVENT_BUFFER_SIZE) eventBuffer.shift();
  return event;
}

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

  const lastId = parseInt(req.headers['last-event-id'] || '0', 10);

  // Replay missed events
  const missed = eventBuffer.filter(e => e.id > lastId);
  for (const event of missed) {
    res.write(`id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`);
  }

  // Register this client to receive future events
  // (production code would use an EventEmitter or pub/sub here)
});

Notice that ChatGPT still leaves the "register client" part as a comment. Follow up immediately with: "Complete the client registration using an EventEmitter so that new events are broadcast to all connected clients, and clean up the listener on connection close." Breaking the prompt into two focused passes reliably produces more complete code than one sprawling request.

Prompting for Correct Buffering and Flush Behavior

Express with compression middleware (compression package) will buffer your SSE writes. The events accumulate and never reach the client. You need an explicit res.flush() after each write, but ChatGPT won't add it unless you mention compression middleware in your prompt.

Prompt pattern: "The Express app uses the compression middleware. Add res.flush() after every res.write() call in the SSE handler to prevent buffering. Also set the X-Accel-Buffering: no response header so that Nginx proxy buffering is disabled for this route."

The resulting code will include both the res.flush() calls and the Nginx header. That header is easy to miss; Nginx's default proxy_buffering on will hold your events until the buffer fills, which for small SSE payloads can mean they never arrive at all.

If you're working in Python with FastAPI, the equivalent prompt needs to mention StreamingResponse and the generator pattern explicitly, because ChatGPT will sometimes generate a non-streaming response by default:

from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def event_generator(request: Request, last_id: int):
    counter = last_id
    while True:
        if await request.is_disconnected():
            break
        counter += 1
        payload = f"id: {counter}\ndata: {{\"tick\": {counter}}}\n\n"
        yield payload
        await asyncio.sleep(1)

@app.get("/events")
async def sse_endpoint(request: Request, last_event_id: int = 0):
    return StreamingResponse(
        event_generator(request, last_event_id),
        media_type="text/event-stream",
        headers={"X-Accel-Buffering": "no", "Cache-Control": "no-cache"}
    )

The request.is_disconnected() check is important: without it, the generator keeps running after the client closes the connection, leaking an async task per abandoned connection. Mention it explicitly in your prompt.

Prompting for Heartbeat and Connection Lifecycle Management

Many reverse proxies and load balancers will terminate idle HTTP connections after 30–60 seconds. SSE connections that have no events to send are effectively idle from the proxy's perspective. The standard solution is a periodic comment line (lines starting with : in SSE format) that keeps the connection alive without triggering client-side event handlers.

Prompt pattern: "Add a heartbeat mechanism that sends an SSE comment (: ping\n\n) every 20 seconds on each connection. Use a separate interval for this that is independent of the data event interval. Cancel both intervals when the connection closes."

ChatGPT handles this cleanly once you spell it out. The key detail in the prompt is "independent of the data event interval" β€” if you don't say that, the model will sometimes reset the heartbeat timer on each data event, which defeats the purpose entirely when data is flowing frequently.

Connection lifecycle is the other side of this. Every SSE connection holds an open socket. If you store client references in a Set or Map for broadcasting, you must remove them on disconnect or you'll accumulate dead references that write to closed sockets. Prompt for this explicitly: "Ensure the client is removed from the active connections Set in the req.on('close') callback, and confirm no further writes occur after that point."

This is the same discipline required when prompting for other stateful server resources. For comparison, prompting for accurate database connection pool configs runs into identical issues with cleanup not being added unless you ask.

Common Pitfalls in ChatGPT-Generated SSE Code

Missing the retry field

The SSE spec lets the server suggest a reconnection delay via the retry: field (in milliseconds). ChatGPT almost never includes this. If your server is restarting or rate-limited, clients will hammer it at the browser's default interval. Prompt: "Send a retry: 5000 field in the first event so the client waits five seconds before reconnecting after a dropped connection."

JSON payload not on a single line

The data: field in SSE must be a single line per field. If you serialize a JSON object with newlines, the browser's EventSource parser will split it across multiple data: fields and reconstruct it incorrectly, or discard it. ChatGPT will sometimes use JSON.stringify(obj, null, 2) in examples. Specify: "Serialize all JSON payloads with JSON.stringify without any formatting arguments so the output is always a single line."

Event name field omitted

If you're sending multiple event types on a single stream, each event should carry an event: field. The client subscribes with addEventListener('eventType', handler). Without it, everything lands on the generic message event and you end up routing in client-side JavaScript. Prompt: "Each event should include an event: field named after its type, so clients can subscribe with addEventListener rather than filtering inside a message handler."

No CORS headers for cross-origin clients

Browser EventSource follows CORS rules. If your SSE endpoint is on a different origin from your frontend, the connection will fail silently in the browser with no visible event to the client code. ChatGPT omits CORS headers on SSE routes unless you ask. If you've already worked through CORS configuration with ChatGPT, apply the same prompting discipline here: specify the allowed origin explicitly rather than accepting a wildcard.

Treating the buffer as a queue instead of a ring

When you ask ChatGPT for a replay buffer, it often uses an array and pushes without bounding it. A server that runs for days will accumulate thousands of events in memory. Specify a fixed-size circular buffer (or an explicit max length with a shift() on overflow) and mention memory constraints in your prompt: "The buffer must never exceed 500 events; discard the oldest when the limit is reached."

This mirrors a pattern seen in prompting ChatGPT for memory leak detection scripts: the model won't constrain resource usage unless your prompt treats it as a hard requirement.

Wrapping Up: Next Steps

SSE is deceptively simple on the surface and genuinely tricky when you account for reconnection, proxies, and backpressure. ChatGPT produces working starter code quickly but defaults to the naive implementation every time. The prompting patterns here close that gap.

  • Add event IDs and replay logic to your prompt up front. Don't expect ChatGPT to infer the reconnection requirement; state it explicitly with a concrete buffer size.
  • Mention your middleware stack. If you use compression or sit behind Nginx, say so. The model adjusts its output to the environment you describe.
  • Prompt for lifecycle cleanup in the same request as the handler. Ask for disconnect handling, buffer bounds, and heartbeat cancellation together so the model sees them as connected requirements.
  • Run a manual reconnection test before calling it done. Drop the client connection mid-stream, wait five seconds, reconnect, and verify the missed events appear. No prompt substitutes for this check.
  • Ask ChatGPT to review its own output for the specific failure modes. After generating the handler, follow up: "Review this code for missing event IDs, unbound memory growth, and connections not cleaned up on close." The self-review pass catches roughly half the remaining issues.

Frequently Asked Questions

Why does my SSE endpoint drop events when the browser reconnects after a network hiccup?

The browser reconnects with a Last-Event-ID header containing the ID of the last event it received, but if your server doesn't read that header and replay missed events, the client starts receiving only new events from that moment forward. You need a server-side buffer and code that filters it on reconnect.

How do I stop Nginx from buffering my SSE responses?

Set the X-Accel-Buffering: no response header on your SSE route, or add proxy_buffering off to the matching Nginx location block. Without one of these, Nginx will hold your event chunks until its buffer fills, causing events to arrive in unpredictable batches or not at all.

Does ChatGPT understand the SSE retry field and when to use it?

ChatGPT knows the retry field exists but almost never includes it in generated code unless you ask. Prompt explicitly for a retry value in milliseconds at the start of the stream so reconnecting clients back off rather than immediately hammering a restarting server.

What is the correct way to send JSON data over SSE without breaking the parser?

Serialize your JSON with JSON.stringify without any formatting arguments so the output is a single line, then write it as a single data: field followed by a double newline. Multi-line JSON splits across multiple data fields and the EventSource parser concatenates them with a newline character, which corrupts the payload.

How do I keep an SSE connection alive through a load balancer that closes idle connections?

Send a comment line, which is a colon followed by any text and a double newline such as ': ping\n\n', every 15 to 20 seconds. Comment lines are valid SSE but trigger no client-side event handlers, so they keep the TCP connection active without affecting your application logic.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.