ShellYard

June 8, 2026 · websocket · sse · api · realtime · devops

Testing WebSocket and Server-Sent Events Endpoints in 2026

Not every API is request/response. Here's how to test WebSocket and SSE endpoints — handshake, auth, message inspection, reconnect logic — and the tools that handle each well.

By ShellYard

The API your team just shipped pushes live updates to clients — order status, telemetry, chat messages, agent reasoning streams. The endpoint isn't /v1/orders with a GET; it's wss://api.example.com/v1/orders/stream with a long-lived connection, or https://api.example.com/v1/events returning Content-Type: text/event-stream and never closing. Standard REST tooling doesn't help much here. Here's how to test both, end to end, including the auth and reconnect details that aren't obvious from the protocol specs.

Two protocols, two shapes

WebSocket is bidirectional. After an HTTP upgrade handshake, the connection switches protocols and both sides can send frames at any time. Used for chat, collaborative editing, game state, live trading, and most things where the client has to push as much as the server does.

Server-Sent Events (SSE) is one-way: server pushes events down a long-lived HTTP response, client just reads. No upgrade — it's plain HTTP with a text/event-stream content type and a streaming body that never ends. Used for notifications, log tails, LLM token streaming, and anything where the client is mostly a passive listener.

The two get conflated because they're both "live updates from the server." They're not interchangeable. WebSocket is heavier, more flexible, and harder to debug with HTTP tooling. SSE is lighter, plays nicely with proxies and load balancers (it's literally just HTTP), and is what most "AI streaming" APIs actually use.

Testing WebSocket — the handshake

A WebSocket connection starts with an HTTP request that asks the server to switch protocols:

GET /v1/orders/stream HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

The server, if it accepts, responds with 101 Switching Protocols and a Sec-WebSocket-Accept header. After that, the TCP connection no longer speaks HTTP — it speaks WebSocket framing in both directions.

You don't write that handshake by hand. Tools that do it for you:

  • websocat — command-line WebSocket client. websocat wss://api.example.com/v1/orders/stream opens a session; type lines to send, see lines come back.
  • wscat — npm package, similar shape. wscat -c wss://….
  • Browser devtools — Chrome's Network tab shows WebSocket frames under the "WS" filter. Useful for inspecting what an existing webapp is exchanging.
  • A dedicated API client — Postman, Insomnia, ShellYard, and others have first-class WebSocket views with a frame log.

The dedicated-client value is mostly in persistent message history. With websocat you see frames scroll past the terminal; close the session and they're gone. A real client logs every send and receive, lets you scroll back, lets you re-send a previous message with one click, and saves the whole session as a "test" you can re-run.

Authenticating a WebSocket

This is where it gets opinionated. The WebSocket protocol itself has no defined auth mechanism. There are three patterns in the wild:

1. Token in a query string. wss://api.example.com/v1/orders/stream?token=eyJhbGciOiJI…. Simple to test (just paste the URL), but the token shows up in proxy logs and browser history. Some teams accept this risk; most internal APIs don't.

2. Token in a custom subprotocol. The Sec-WebSocket-Protocol header is part of the handshake and can carry an auth value. Some servers parse it as Sec-WebSocket-Protocol: Bearer, eyJhbGciOiJI…. The browser WebSocket API supports specifying this via the second arg: new WebSocket(url, ["Bearer", token]). Not standard, but common.

3. Cookie or HTTP header on the handshake request. Because the upgrade is an HTTP request, anything you can do on an HTTP request — Authorization: Bearer …, session cookies, mTLS — works on the upgrade. Most browser code can't set arbitrary headers, but server-to-server and desktop API clients can. This is the most "correct" path.

A capable WebSocket client lets you set headers on the upgrade request. If your client only supports the query-string pattern, ask why.

What to send once you're connected

You're connected; the prompt blinks. What do you send? It depends entirely on the server. Common patterns:

  • JSON messages with a type field. {"type":"subscribe","channel":"orders"} is typical. The server responds with messages of various types — {"type":"order.created","payload":{…}}.
  • GraphQL subscriptions over WebSocket. Uses a specific subprotocol (graphql-ws or the older graphql-transport-ws). Frames carry connection_init, subscribe, next, complete. If you're testing a GraphQL subscription, your client needs to speak this — generic WebSocket tools can't.
  • Custom binary framing. Some protocols send binary frames (protobuf, MessagePack). You need a tool that can show hex/binary, not just text.

The right move on first connect is to send whatever the server expects to bootstrap a session (connection_init, subscribe, hello, whatever the docs say), then watch frames come back. Iterate from there.

Testing Server-Sent Events

SSE is much simpler than WebSocket because it's literally HTTP. You can hit an SSE endpoint with curl:

curl -N https://api.example.com/v1/events \
  -H "Authorization: Bearer eyJhbGciOiJI…" \
  -H "Accept: text/event-stream"

The -N (no buffering) is important — without it, curl waits for the response to finish before printing, and an SSE response never finishes by design. With -N, you see events as they arrive.

The wire format is text:

event: order.created
data: {"id":"123","total":42}
id: 12345

event: order.updated
data: {"id":"123","status":"paid"}
id: 12346

Lines are key/value pairs. event: names the event type. data: carries the payload (usually JSON, but it's whatever the server sends). id: is the event ID, used for reconnect (see below). A blank line ends an event.

For testing, curl -N is enough for "is the server actually streaming." For development, a dedicated SSE client parses the events for you, decodes the JSON, lets you filter by event type, and shows a timeline of when each arrived.

SSE auth and the reconnect rules

SSE uses standard HTTP auth — Authorization: Bearer …, cookies, anything you'd put on a normal request. That's a real advantage over WebSocket; you don't have to invent a pattern.

The reconnect logic is part of the spec. If the connection drops, the client is supposed to:

  1. Wait a moment (default: 3 seconds; server can suggest different with retry: field).
  2. Reconnect to the same URL.
  3. Include the Last-Event-ID HTTP header with the ID of the last event it processed.

The server, if it's well-behaved, looks at Last-Event-ID and resumes the stream from after that event — no events lost. If the server doesn't support resume, the client gets a fresh stream and may have missed events during the disconnect.

When testing an SSE endpoint, explicitly test reconnect. Drop your network, wait, restore it, and see whether your client correctly sends Last-Event-ID and whether the server correctly resumes. Half of all "SSE works in dev but loses events in prod" tickets trace back to one or the other not implementing this.

When SSE is the wrong choice — and people use it anyway

A few real gotchas:

  • Browsers limit concurrent SSE connections per origin to 6. Open seven tabs to the same SSE endpoint and the seventh hangs. Solved with the more recent Server-Sent Events over HTTP/2 (which multiplexes) but the old limit still bites in HTTP/1.1 deployments.
  • Some corporate proxies buffer streaming responses. Your SSE endpoint serves events instantly; the proxy holds them until it has a few KB or a timeout. From the client's perspective the stream looks frozen. Setting X-Accel-Buffering: no (for nginx) or appropriate equivalents helps.
  • SSE is server → client only. If your "client → server message" path is "the client opens a separate HTTP POST," fine. If you need real bidirectional comms, you wanted WebSocket.

Tools, ranked by what they're good at

  • curl -N for SSE. Hard to beat for "is it actually streaming" sanity checks. Cannot do WebSocket — it's HTTP/1.1 only.
  • websocat / wscat for WebSocket sanity checks. Connect, send, see. No history, no decoding, no auth presets — fine for one-offs.
  • Browser devtools. Best for inspecting what an existing webapp is doing — you see exactly the frames the production client sends and receives.
  • Dedicated API clients (Postman, Insomnia, ShellYard). Best for iterative development — frame log, replay, save as a test, auth presets, JSON pretty-print on data: lines, persistent across runs.

For testing WebSocket and SSE alongside your REST and GraphQL work, in one window with the same auth presets shared across all four protocols, ShellYard's Realtime view handles both with a frame log, a JSON-pretty-printer on inbound messages, and proper handling of Last-Event-ID on SSE reconnect. Free, local, no account: download it here.

Where this fits in the cluster

This closes the 5-post arc on what modern API testing actually has to cover. The pillar named the surface; the four spokes drilled into each piece:

If your WebSocket or SSE endpoint lives behind a bastion or VPN — common for internal AI services and trading APIs — the companion guide on testing APIs behind a bastion covers routing live connections through an SSH session, which works the same way for wss:// as it does for https://.

Send the next message into the stream.