open-webui/backend/open_webui
Claude d7b36be54d
perf(stream): emit text tokens as deltas instead of re-serializing the whole chat per SSE event
The streaming hot path in `streaming_chat_response_handler.stream_body_handler`
was calling `serialize_output(full_output())` on every SSE event — rebuilding
an HTML string of the entire accumulated output (text + reasoning + tool calls
+ images + citations) and emitting it via Socket.IO.

For an N-token response this is O(N) work per token -> O(N^2) bytes across
the WebSocket, then amplified again by Socket.IO's AsyncRedisManager pub/sub
(x redis nodes x workers) and a third time by the frontend Markdown
re-parser. A 2000-token reply at normal scale could push ~10,000x more
bytes through Redis than the new tokens actually needed.

Fix: when an SSE event only appended text characters to the currently
active `message` block (the overwhelming common case), accumulate those
chars into a `pending_text_delta` buffer and emit them as a lightweight
`chat:message:delta` event. The frontend already handles that event via
string append (`Chat.svelte:472-473`), so no frontend changes are needed.

Any structural event - reasoning update, reasoning->message transition,
tool call, image attachment, tag-block append, annotation source, etc. -
still goes through the legacy full-serialize path, and when it does it
clears the pending text delta buffer (because the full-content checkpoint
already contains the accumulated text). Ordering is preserved by flushing
pending deltas before each structural emit.

The final `done: true` `chat:completion` emitted by the outer handler at
line 4793 overwrites `message.content` with the canonical
`serialize_output(output)`, reconciling the frontend to the exact backend
state at the end of every response.

Discovered and diagnosed by @Shirasawa; this is the Phase 1 minimum-viable
landing. Phase 2 (typed block model, per-block rendering, Redis op log
with resume-by-seq) is tracked as follow-up work.

Impact summary:
- Per-token WS payload: O(size_so_far) -> O(new_chars)
- Redis pub/sub bytes: drops by ~10-100x on long responses
- Frontend Markdown re-parse cost: now bounded by delta size per event,
  not full response size
- Semantics: identical. Final message content reconciles via the
  existing `done: true` full-content checkpoint.

Out of scope for this change:
- Reasoning-block content still takes the full-serialize path per token
  (Phase 2 will incrementalize it via per-block deltas)
- Tool-call argument streaming still takes the full-serialize path
- DB persistence path (REALTIME_CHAT_SAVE) is untouched — its WS path
  already emitted raw SSE deltas and is unchanged
2026-04-14 20:33:02 +00:00
..
data refac: mv backend files to /open_webui dir 2024-09-04 16:54:48 +02:00
internal refac 2026-04-12 19:41:02 -05:00
migrations refac 2026-04-01 18:26:46 -05:00
models refac 2026-04-13 21:29:03 -05:00
retrieval fix(retrieval): offload Loader.load to a worker thread so file uploads stop blocking the event loop (#23705) 2026-04-14 10:55:46 -05:00
routers fix(retrieval): offload Loader.load to a worker thread so file uploads stop blocking the event loop (#23705) 2026-04-14 10:55:46 -05:00
socket fix: invalidate stale Socket.IO sessions on role change and user deletion (#23642) 2026-04-12 16:19:38 -05:00
static refac 2026-03-23 23:39:52 -05:00
storage refac 2026-04-12 19:08:30 -05:00
test refac 2026-03-17 17:58:01 -05:00
tools fix(retrieval): offload sync VECTOR_DB_CLIENT calls in async paths via AsyncVectorDBClient (#23706) 2026-04-14 10:50:18 -05:00
utils perf(stream): emit text tokens as deltas instead of re-serializing the whole chat per SSE event 2026-04-14 20:33:02 +00:00
__init__.py refac (#22987) 2026-03-24 15:41:26 -05:00
alembic.ini fix: Alembic CLI commands from failing 2025-08-15 04:17:47 -04:00
config.py refac 2026-04-14 00:07:50 -05:00
constants.py refac 2026-04-13 14:08:58 -05:00
env.py refac 2026-04-13 16:25:44 -05:00
functions.py refac: async db 2026-04-12 14:22:11 -05:00
main.py fix(middleware): replace BaseHTTPMiddleware HTTP middlewares with pure ASGI implementations (#23709) 2026-04-14 10:47:48 -05:00
tasks.py refac 2026-03-17 17:58:01 -05:00