n4nAI

Testing SSE streams with curl for LLM APIs

Learn how to test SSE stream curl LLM API endpoints with practical curl commands, parse raw event streams, and debug streaming quirks.

n4n Team3 min read734 words

Audio narration

Coming soon — every post will get a voice note here.

When you need to test sse stream curl llm api endpoints, curl is the fastest way to see raw bytes without an SDK masking behavior. Streaming Server-Sent Events from LLM APIs have quirks—chunked encoding, partial JSON, and keep-alive comments—that a quick Python script hides.

Prerequisites

  • curl 7.68 or newer (supports --no-buffer and HTTP/2 cleanly)
  • jq 1.6+ for JSON parsing
  • python3 for any custom post-processing
  • A valid API key for an OpenAI-compatible endpoint
  • Basic comfort with bash pipes

If you are hitting a gateway, the same patterns apply. The examples below use the standard /v1/chat/completions shape.

The minimal streaming request

A streaming chat completion requires stream: true and the Accept: text/event-stream header. Curl must disable buffering with -N or --no-buffer, otherwise it will wait for the full response.

curl -N https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "model": "gpt-3.5-turbo",
    "stream": true,
    "messages": [{"role": "user", "content": "Say hello in 5 words."}]
  }'

Expected raw output (truncated):

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl-123","choices":[{"index":0,"delta":{"content":" there"},"finish_reason":null}]}

data: [DONE]

Note the blank lines between data: frames. That is part of the SSE spec: each event is terminated by a double newline.

Understanding the raw SSE frame

SSE frames can carry event:, data:, id:, and retry: lines. LLM APIs almost always send only data: with a JSON payload. Lines starting with a colon are comments used as keep-alives:

: ping

If you see : ping every 15–30 seconds, the server is keeping the TCP connection alive. Curl prints these to stdout just like data lines. To filter them:

curl -N ... | grep '^data:'

But the [DONE] sentinel is also prefixed with data:. Strip the prefix and drop the sentinel for clean JSON:

curl -N ... | sed 's/^data: //' | grep -v '^\[DONE\]'

Extracting tokens with a one-liner

To watch just the generated text, parse each JSON line and pull choices[0].delta.content. jq will choke on the [DONE] line, so filter first.

curl -N https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"model":"gpt-3.5-turbo","stream":true,"messages":[{"role":"user","content":"Count to 3."}]}' \
  | sed 's/^data: //' \
  | grep -v '^\[DONE\]' \
  | jq -r '.choices[0].delta.content // empty'

Output:

One
Two
Three

If your model returns usage info in the final chunk (common in newer OpenAI-compatible servers), the last JSON line has a usage field and no delta.content. The // empty guard avoids null prints.

Inspecting headers and connection behavior

Streaming bugs often live in headers. Use -v to see the handshake:

curl -N -v https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{"model":"gpt-3.5-turbo","stream":true,"messages":[{"role":"user","content":"Hi"}]}' 2>&1 | head -20

Look for:

< HTTP/2 200
< content-type: text/event-stream
< transfer-encoding: chunked
< cache-control: no-cache

If you get content-encoding: gzip with a streaming response, some proxies misbehave. Curl decompresses automatically, but intermediate layers may buffer. Force uncompressed with --compressed only if you control both ends; otherwise ask the gateway to disable compression for streams.

Measuring time-to-first-token

A common SLO is latency to first byte. Curl’s -w does not capture streaming intervals well, but you can timestamp each line:

curl -N ... | while IFS= read -r line; do
  if [[ "$line" == data:* ]]; then
    echo "$(date +%s.%N) $line"
  fi
done | head -3

For a proper TTFB, start a timer before curl and print on first data: that contains content:

start=$(date +%s.%N)
curl -N ... | while IFS= read -r line; do
  if [[ "$line" == data:* ]] && [[ "$line" == *"\"content\""* ]]; then
    echo "first token at $(date +%s.%N), delta $(echo "$(date +%s.%N) - $start" | bc)"
    break
  fi
done

Simulating client disconnects

You want to know if the server stops generating when the client hangs up. Pipe to head -c to truncate early:

curl -N ... | head -c 200

Curl receives SIGPIPE when head exits; the connection closes. Watch server logs for cancelled requests. If you are behind a load balancer, confirm it propagates the RST and does not keep spawning tokens.

Testing routing and fallback through a gateway

When you test sse stream curl llm api against a gateway such as n4n.ai, which exposes one OpenAI-compatible endpoint across 240+ models with automatic fallback when a provider is degraded, you can assert routing with a custom header and inspect the response headers for which backend served the stream. Send a client routing directive and watch the x-routed-provider (or similar) header:

curl -N -D - https://api.n4n.ai/v1/chat/completions \
  -H "Authorization: Bearer $N4N_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "x-n4n-route: provider=anthropic" \
  -d '{"model":"claude-3-haiku","stream":true,"messages":[{"role":"user","content":"ping"}]}' \
  | grep -i 'x-routed-provider\|^data:'

If the primary provider is rate-limited, the gateway should fail over before or at stream start; the header tells you which backend actually responded. This is impossible to verify through a high-level SDK that swallows headers.

Common pitfalls

Buffering in intermediate shells. python -u or stdbuf -oL may be needed if you wrap curl in a script.

JSON escaping in -d. Use a heredoc or @file.json to avoid shell mangling:

cat > req.json <<'EOF'
{"model":"gpt-3.5-turbo","stream":true,"messages":[{"role":"user","content":"Hello"}]}
EOF
curl -N https://api.openai.com/v1/chat/completions -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -H "Accept: text/event-stream" -d @req.json

Missing Accept header. Some gateways return a single JSON blob if they think you are not streaming.

TLS session reuse. HTTP/2 connection coalescing can mask per-request routing. Use --http1.1 if you need isolated connections.

Keep-alive comments breaking parsers. Always skip lines not starting with data:.

A reusable debug function

Drop this in your .bashrc to standardize how you test sse stream curl llm api calls:

llm-stream() {
  local url="${1:-https://api.openai.com/v1/chat/completions}"
  local key="${2:-$OPENAI_API_KEY}"
  local body="${3:-{\"model\":\"gpt-3.5-turbo\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"Say hi\"}]}}"
  curl -N "$url" \
    -H "Authorization: Bearer $key" \
    -H "Content-Type: application/json" \
    -H "Accept: text/event-stream" \
    -d "$body" \
    | sed 's/^data: //' \
    | grep -v '^\[DONE\]' \
    | jq -r '.choices[0].delta.content // empty'
}

Call it with llm-stream https://api.n4n.ai/v1/chat/completions "$N4N_KEY" '@req.json'. You get a clean token feed and can swap endpoints to compare providers.

Verifying cache-control hints

OpenAI-compatible servers may emit cache-control or accept x-cache-key. When you forward provider cache-control hints through a gateway, confirm the header survives. Use -D - and grep:

curl -N -D - ... | grep -i 'cache-control\|x-cache'

If the stream response carries cache-control: no-transform, your CDN should not buffer it.

Final checklist

Before you trust a streaming integration, run these three commands:

  1. curl -N -v to confirm text/event-stream and chunked encoding.
  2. curl -N | sed | jq to confirm parseable JSON and clean token extraction.
  3. curl -N -D - with routing header to confirm backend selection and fallback behavior.

That is the entire surface area for debugging most LLM streaming issues without writing a line of application code.

Tagsssecurltestingstreaming

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All debugging streaming responses posts →