SSE and Quota Management¶
Server-Sent Events (SSE) keep an HTTP response open while an inference engine streams tokens. LoxiLB protects this long-lived path from normal idle reaping, requests usage accounting, and settles token-quota reservations when the response completes.
Quota enforcement requires the independent key store
SSE relay behavior is part of fullproxy, but API-key and token-quota admission uses the
PostgreSQL store configured by --aikey-db-*, not the management user service. With no
--aikey-db-host, the current data path admits requests without key checks. Verify a
missing-key request returns 401 before relying on quota enforcement.
Stream lifecycle¶
sequenceDiagram
participant Client
participant Gateway as LoxiLB Gateway
participant Engine as Inference engine
Client->>Gateway: POST with stream=true and X-Api-Key
Note over Gateway: Authenticate and reserve<br/>prompt estimate + completion ceiling
Gateway->>Engine: Forward request with usage reporting enabled
Engine-->>Gateway: Content-Type: text/event-stream
loop Generated chunks
Engine-->>Gateway: data: {...}
Gateway-->>Client: Relay chunk
end
Engine-->>Gateway: usage and data: [DONE]
Note over Gateway: Release reservation<br/>charge actual or estimated tokens
Gateway-->>Client: Final chunks and close
With mode: 4 and sse_mode: true, the Gateway:
- suppresses ordinary inactivity reaping while an SSE response is active;
- detects the OpenAI-compatible
data: [DONE]terminator; - applies an absolute stream-duration ceiling;
- can enable backend TCP keepalive for network idle periods;
- reserves tokens before dispatch and settles usage after completion;
- excludes time paused by the configured fullproxy byte shaper from idle and stream-duration accounting.
Configuration fields¶
| Field | Default | Meaning |
|---|---|---|
sse_mode |
false |
Enables event-stream lifecycle and AI Gateway request handling |
max_stream_duration_sec |
0 |
Absolute limit in seconds; 0 uses the system ceiling of 86,400 seconds |
backend_keepalive_interval_sec |
0 |
Backend TCP keepalive idle interval; 0 disables it |
inactiveTimeOut |
Rule default | Normal inactivity timeout; suppressed while an SSE stream is active |
Choose a finite stream duration that covers legitimate generations and still bounds stuck connections. Set keepalive from your network's measured idle behavior rather than copying a value blindly.
Configure a streaming rule¶
Use TLS and a protected control-plane header outside an isolated lab:
export CONTROL_API="https://gateway.example.com/netlox/v1"
install -m 600 /dev/null ./control-plane.headers
printf 'Authorization: Bearer %s\n' "$CONTROL_PLANE_TOKEN" > ./control-plane.headers
The example addresses use documentation-only ranges. Replace all addresses and the model with your environment:
curl --fail-with-body --silent --show-error \
--request POST "$CONTROL_API/config/loadbalancer" \
--header @control-plane.headers \
--header 'Content-Type: application/json' \
--data '{
"serviceArguments": {
"externalIP": "192.0.2.20",
"port": 2020,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "192.0.2.20",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "example-stream-model",
"sse_mode": true,
"max_stream_duration_sec": 300,
"backend_keepalive_interval_sec": 60,
"inactiveTimeOut": 60
},
"endpoints": [
{"endpointIP": "198.51.100.20", "targetPort": 8000, "weight": 1}
]
}'
Read the rule back and confirm mode, sse_mode, duration, keepalive, and
model before sending traffic.
Verify streaming and quota accounting¶
Store the inference key in a protected header file:
install -m 600 /dev/null ./inference.headers
printf 'X-Api-Key: %s\n' "$INFERENCE_API_KEY" > ./inference.headers
curl --no-buffer --fail-with-body --silent --show-error \
--header @inference.headers \
--header 'Content-Type: application/json' \
--data '{
"model": "example-stream-model",
"stream": true,
"stream_options": {"include_usage": true},
"messages": [{"role": "user", "content": "Reply briefly."}],
"max_tokens": 32
}' \
"https://ai.example.com/v1/chat/completions"
A healthy stream produces incremental data: records, a terminal usage
object, and [DONE]. The active-stream gauge rises during the request and
returns after completion. Token counters increase for the tenant and model.
The Gateway can request usage reporting and reserve prompt/completion tokens
only after buffering a complete, contiguous, positive-Content-Length JSON
body. Chunked, partial, and oversized bodies skip parsing and
include_usage injection. If usage is absent or unreadable, the fallback
estimate can undercount the actual prompt; the estimated-token and
missing-usage series are signals to verify framing and engine compatibility.
Quota behavior for streams¶
For a complete, contiguous, positive-Content-Length JSON body, admission
reserves the prompt estimate plus the declared completion ceiling. Both
aggregate tenant TPM and tenant-and-model TPM must have enough capacity. After
completion, the reservation is credited back and replaced with the actual
extracted charge or the estimate. Chunked, partial, and oversized bodies skip
pre-admission reservation and settle afterward from readable usage or the
fallback estimate, which may undercount prompt use.
If a final charge creates debt, the already-served stream is not interrupted;
later requests receive 429 until continuous refill restores headroom. An
eligible buffered request that cannot fit before dispatch receives 429
without consuming GPU work. This pre-dispatch guarantee does not apply to the
skipped body shapes above.
While peer quota state is warming, the gateway returns
429 token_quota_warming with Retry-After: 1. The default warm-up deadline is
three seconds. If no peer state arrives before that deadline, the current
compatibility path fails open; alert on this event and verify synchronization
instead of treating the timeout as healthy quota state.
See AI Traffic Governance for burst_pct, model
limits, and status-code diagnosis.
Shaping interaction¶
A policer attached to a fullproxy rule becomes a bidirectional L7 payload
shaper. A shaped stream may take longer than the configured duration in wall
time because periods deliberately paused by the shaper are excluded. An
unshaped slow stream is still bounded by max_stream_duration_sec.
This exclusion prevents the Gateway's own pacing policy from being mistaken for an idle or runaway stream. It does not disable duration protection for backend-generated slowness.
Troubleshooting¶
| Symptom | Likely cause | Action |
|---|---|---|
| Stream ends during a quiet gap | SSE handling is off or response is not recognized as event-stream | Confirm mode: 4, sse_mode: true, and backend content type |
| Connection remains open | Backend omitted [DONE] or failed to close |
Inspect sanitized backend output and keep a finite duration cap |
| Long streams fail across a network idle period | Intermediary removed the backend flow | Configure and validate backend keepalive; also check external proxy timeouts |
| Stream ends at a fixed duration | Absolute cap reached | Increase only after confirming legitimate generation time |
| Estimated/missing metrics rise | Usage object absent, split, or incompatible | Verify engine usage format and streaming configuration |
Request receives 429 before backend traffic |
Reservation exceeds current aggregate or model headroom | Check declared completion ceiling, burst_pct, and utilization |
Stream completes but next request gets 429 |
Final usage produced quota debt | Wait for refill or correct an undersized limit; do not retry in a tight loop |
| Shaped stream survives beyond wall-clock cap | Time was paused by the shaper | Expected; confirm QoS park metrics and effective policy |
Security and cleanup¶
Streaming bodies can contain sensitive prompts and model output. Do not capture
complete streams in routine logs. Redact X-Api-Key, authorization headers,
prompt content, and usage correlated with customer identities.
Delete the example rule using its complete key, including model_name when the
delete route requires the keyed model value. Then remove local secret files: