Harness-engineering events-sse-pattern

Events: SSE Pattern

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/events-sse-pattern" ~/.claude/skills/intense-visions-harness-engineering-events-sse-pattern && rm -rf "$T"
manifest: agents/skills/claude-code/events-sse-pattern/SKILL.md
source content

Events: SSE Pattern

Stream one-way server events to browsers using Server-Sent Events and EventSource.

When to Use

  • You need to push real-time updates from server to browser (one-way: server → client only)
  • Live notifications, activity feeds, progress indicators, live scores, stock tickers
  • You want auto-reconnect behavior built into the browser for free
  • You prefer HTTP/1.1 compatibility over WebSocket's upgrade handshake
  • NOT when the client needs to push data back to the server — use WebSocket for bidirectional communication

Instructions

Express SSE endpoint:

import express, { Request, Response } from 'express';

const app = express();

// SSE connection handler
app.get('/events', (req: Request, res: Response) => {
  // Required headers for SSE
  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
  res.flushHeaders(); // send headers immediately

  // Send a comment every 30s to keep the connection alive through proxies
  const keepAlive = setInterval(() => {
    res.write(': keep-alive\n\n');
  }, 30_000);

  // Helper to send typed events
  const send = (eventType: string, data: unknown, id?: string): void => {
    if (id) res.write(`id: ${id}\n`);
    res.write(`event: ${eventType}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  // Send initial state
  send('connected', { timestamp: new Date().toISOString() });

  // Register this client for notifications
  const userId = req.query.userId as string;
  const clientId = addClient(userId, send);

  // Clean up when client disconnects
  req.on('close', () => {
    clearInterval(keepAlive);
    removeClient(clientId);
    console.log(`SSE client ${clientId} disconnected`);
  });
});

// Client registry
type SendFn = (event: string, data: unknown, id?: string) => void;
const clients = new Map<string, { userId: string; send: SendFn }>();

function addClient(userId: string, send: SendFn): string {
  const clientId = crypto.randomUUID();
  clients.set(clientId, { userId, send });
  return clientId;
}

function removeClient(clientId: string): void {
  clients.delete(clientId);
}

// Push notification to a specific user's SSE streams
function pushToUser(userId: string, event: string, data: unknown): void {
  for (const [, client] of clients) {
    if (client.userId === userId) {
      client.send(event, data, crypto.randomUUID());
    }
  }
}

// Broadcast to all connected clients
function broadcast(event: string, data: unknown): void {
  for (const [, client] of clients) {
    client.send(event, data);
  }
}

// Trigger from anywhere in your application
pushToUser('user-123', 'notification', {
  title: 'Your order shipped',
  message: 'Order #456 is on its way',
  timestamp: new Date().toISOString(),
});

SSE with Last-Event-ID for resumption:

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

  // Resume from last seen event ID (browser sends automatically on reconnect)
  const lastEventId = req.headers['last-event-id'] as string | undefined;

  if (lastEventId) {
    // Replay missed events
    const missed = await db.events.findMany({
      where: { id: { gt: lastEventId }, userId: req.user.id },
      orderBy: { id: 'asc' },
      take: 100,
    });

    for (const event of missed) {
      res.write(`id: ${event.id}\nevent: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
    }
  }

  // Continue real-time stream...
});

Browser client:

// Browser — EventSource handles reconnection automatically
const es = new EventSource('/events?userId=user-123');

es.addEventListener('notification', (event: MessageEvent) => {
  const data = JSON.parse(event.data);
  showNotification(data.title, data.message);
});

es.addEventListener('order.updated', (event: MessageEvent) => {
  const data = JSON.parse(event.data);
  updateOrderStatus(data.orderId, data.status);
});

es.addEventListener('error', (event) => {
  console.error('SSE error — browser will retry automatically');
});

// EventSource reconnects automatically on disconnect
// It sends Last-Event-Id header to resume from where it left off

// Cleanup
function disconnect(): void {
  es.close();
}

Details

SSE protocol format:

id: <unique-event-id>\n
event: <event-type>\n
data: <json-string>\n
\n                     ← blank line terminates the event

Auto-reconnect: Browser's EventSource retries after 3 seconds by default. Set custom retry interval:

retry: 5000\n
\n

Anti-patterns:

  • Sending large payloads via SSE — SSE is for small notifications, not binary data transfers
  • Not setting
    X-Accel-Buffering: no
    — nginx buffers SSE responses, breaking the stream
  • No keep-alive — many proxies close idle connections after 30-60 seconds

Scaling SSE: SSE connections are long-lived HTTP connections. Each connection holds a socket on the server. Use Redis pub/sub to distribute events across multiple server instances:

// When running multiple Node instances
const redisSub = new Redis(REDIS_URL);
redisSub.subscribe('push.events');
redisSub.on('message', (_channel, message) => {
  const { userId, event, data } = JSON.parse(message);
  pushToUser(userId, event, data); // push to local clients only
});

// Publisher (any service instance)
const redisPub = new Redis(REDIS_URL);
await redisPub.publish('push.events', JSON.stringify({ userId, event: 'notification', data }));

SSE vs. WebSocket comparison:

SSEWebSocket
DirectionServer → ClientBidirectional
ProtocolHTTPWS upgrade
Auto-reconnectYes (built-in)Manual
Browser supportExcellentExcellent
Proxy supportBetter (HTTP)Variable

Source

html.spec.whatwg.org/multipage/server-sent-events.html

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. Verify your implementation against the details and edge cases listed above.

Harness Integration

  • Type: knowledge — this skill is a reference document, not a procedural workflow.
  • No tools or state — consumed as context by other skills and agents.

Success Criteria

  • The patterns described in this document are applied correctly in the implementation.
  • Edge cases and anti-patterns listed in this document are avoided.