n4nAI

Why streaming responses need different SLOs than batch APIs

Streaming and batch LLM APIs fail differently. Learn why SLOs for streaming vs batch LLM APIs must track TTFT, token latency, and job completion separately.

n4n Team4 min read889 words

Audio narration

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

A chat UI that stalls for three seconds between words feels broken; a batch job that finishes an hour late is invisible to users. That asymmetry is why SLOs for streaming vs batch LLM APIs must be defined on completely different axes—interactivity versus throughput—and treating them as one category leads to misleading alerts and wasted capacity.

The user is waiting for different things

In a streaming deployment, the client renders output as soon as the first token lands. The contract is perceptual: the user expects near-immediate acknowledgement and a steady cadence. A 10-second full response is fine if the first token appears in 300 ms and the rest flow at 20+ tokens/sec.

Batch is the opposite. You submit 50,000 prompts in a file, get a job ID, and a human reads the results the next morning. The user never sees intermediate tokens. The contract is “all rows eventually correct.” A 30-minute delay is irrelevant; a 0.1% silent drop is catastrophic.

Mixing these into a single “API latency” SLO hides the only numbers that matter to each workload.

Why p99 end-to-end latency fails both modes

Set a blanket p99 latency SLO of 2 seconds on an LLM endpoint and watch what happens.

For streaming chat, a 200-token response generated at 25 tokens/sec takes 8 seconds of generation alone. The p99 “response time” will always breach. Your on-call gets paged for a healthy system. Worse, the metric says nothing about whether the user saw a fast first token or suffered a 5-second gap mid-sentence.

For batch, the endpoint returns a job handle in 200 ms. p99 looks great. Meanwhile 2% of jobs silently stall because a provider threw 429s and your client gave up. The SLO is green; the data pipeline is broken.

The metric must match the failure mode.

import time

start = time.monotonic()
first_token_seen = False
ttft = None
gaps = []

prev = None
for chunk in stream_completion(prompt):
    now = time.monotonic()
    if not first_token_seen:
        ttft = now - start
        first_token_seen = True
    else:
        gaps.append(now - prev)
    prev = now

p95_gap = sorted(gaps)[int(0.95 * len(gaps))]

This loop measures what streaming users feel. Batch needs no such loop—it needs a state poll.

Streaming SLOs: TTFT, ITL, and goodput

Define three independent streaming objectives:

  1. Time to first token (TTFT): 95% of requests emit first token < 500 ms for conversational models.
  2. Inter-token latency (ITL): 90% of gaps between tokens < 40 ms (≈25 tokens/sec).
  3. Goodput: 99% of sessions deliver ≥ 95% of expected tokens without a single gap > 2 s.

TTFT captures “did it acknowledge me.” ITL captures “does it stutter.” Goodput captures “did it actually finish the thought.”

A gateway that forwards provider cache-control hints keeps TTFT low for repeated prefixes without custom client logic. n4n.ai forwards those hints, so a cached system prompt stays cheap and fast across requests.

{
  "slo": "streaming_ttft",
  "objective": "p95 < 0.5s",
  "alert": {
    "condition": "p95(ttft) > 0.5 for 5m",
    "severity": "page"
  }
}

Do not alert on full-response time. By the time you compute it, the user has already closed the tab.

Batch SLOs: acceptance, completion, and durability

Batch workloads care about job lifecycle, not token cadence.

  • Acceptance: 99.9% of submit calls return 202 with a valid job ID.
  • Completion: 99% of jobs reach terminal state within the documented window (e.g., 24 h).
  • Integrity: 100% of output rows map to input rows; <0.01% corrupted or missing.

These are evaluated by a worker polling job status, not by user-perceived latency.

curl -X POST https://api.example.com/v1/batch \
  -H "authorization: Bearer $KEY" \
  -d '{"requests":[...], "completion_window":"24h"}'
# => {"job_id":"batch_123","status":"accepted"}

For batch, provider degradation is less visible but more dangerous; a gateway like n4n.ai that performs automatic fallback when a provider is rate-limited keeps completion SLOs intact without client-side retry storms.

{
  "slo": "batch_completion",
  "objective": "p99 jobs terminal within 24h",
  "alert": {
    "condition": "count(status=running and age>24h) > 0",
    "severity": "ticket"
  }
}

Tradeoffs: strict streaming SLOs cost more

Provisioning for a 500 ms TTFT p95 means keeping model replicas warm and sizing for peak concurrent streams. You cannot let instances scale to zero. Batch has no such constraint—it can run on preemptible capacity, because a job delayed 10 minutes is still inside SLO.

If finance sees one “LLM API” line item, they will either reject the streaming buffer or starve batch. Separate SLOs let you assign separate quotas, separate instance pools, and separate cost attribution. Per-token metering (which any serious gateway provides) makes that split auditable.

Monitoring architecture that respects the split

Streaming metrics must be emitted incrementally. A span that only finishes at the last token hides TTFT until it’s history. Use OpenTelemetry spans that update attributes as tokens arrive.

from opentelemetry import trace
tracer = trace.get_tracer("llm_stream")

with tracer.start_as_current_span("completion") as span:
    span.set_attribute("ttft_ms", ttft * 1000)
    for i, gap in enumerate(gaps):
        span.add_event("token", {"seq": i, "gap_ms": gap * 1000})

Batch monitoring is a state machine: accepted → running → completed/failed. Alert on age, not on latency. Track the count of jobs older than the completion window still in running state.

A shared dashboard with a single “latency” panel will train engineers to ignore it. Build two boards. Link them only at the top-level “LLM platform health” summary.

Honest tradeoffs of splitting SLOs

You now maintain two dashboards, two alert pipelines, and likely two owning teams. Incident response needs context: a streaming page at 2 a.m. is a different animal than a batch ticket at 10 a.m. There is organizational overhead.

But the alternative—a blended SLO—produces alerts nobody trusts. In practice, the split reduces paging volume because streaming blips stop triggering batch alarms, and batch stalls stop waking streaming on-call. The clarity pays for itself in the first week.

Takeaway

If you measure streaming and batch LLM traffic with the same SLO, you are measuring the wrong thing. Define SLOs for streaming vs batch LLM APIs on their native axes: TTFT and token cadence for interactive use, acceptance and completion deadlines for batch. Build separate instrumentation, separate alerts, and separate capacity plans. Do that, and your latency numbers will finally mean something to the people who depend on them.

Tagslatencystreamingsloperformance-monitoring

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 latency & streaming performance monitoring posts →