Adds a bounded Redis stream log for every in-flight assistant message so clients can reconnect mid-stream (page refresh, network drop, device switch) and catch up on frames they missed without re-fetching the full chat from the database.
Problem this solves
With ENABLE_REALTIME_CHAT_SAVE=False (default), the backend does not write the assistant message to the DB until the stream finishes. If the client refreshes the page mid-stream, chat load from the DB returns nothing for the in-progress message and the response appears to vanish until the stream eventually completes. Users staring at an empty chat while the backend quietly keeps emitting tokens into the void.
Design
Every outbound WS envelope gets stamped with a monotonic per-message seq inside get_event_emitter and appended to a bounded Redis stream keyed {REDIS_KEY_PREFIX}:stream:{message_id}. MAXLEN ~ 2000 entries, TTL 1h as a safety net.
Clients track message.lastSeq in Chat.svelte as events arrive. On chat load (mid-stream refresh) and on socket reconnect they emit resume-stream {chat_id, message_id, last_seq}.
The server authenticates the session, verifies the user owns the chat, XRANGEs the log, filters by seq > last_seq, and emits the missed envelopes to THAT session only (via to=sid) so live listeners in the user room keep receiving their normal live stream unchanged.
The existing chat event handler drops any envelope with seq <= message.lastSeq, making replay idempotent against live frames that race the replay after a reconnect.
When an event with done: True fires, a background task truncates the log after a 30s grace window so late reconnects still catch the finalization; anything beyond that resumes from the now-up-to-date DB.
Orthogonality
Zero touches to middleware.py or the streaming hot path. The log stores whatever gets emitted; any future change to emit shape (chat:message:delta, per-block ops, JSON Patch, ...) is logged and replayed verbatim with no coupling.
Graceful degradation
No-op when Redis is not configured (WEBSOCKET_MANAGER != 'redis'). In that deployment mode, refresh during streaming retains the current behavior of waiting for the stream to complete.
Auth model
The log is keyed by message_id only. The resume handler must do a chat ownership check (Chats.get_chat_by_id_and_user_id) before replaying, so a malicious client cannot read another user's stream by guessing a message_id.
Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the Contributor License Agreement (CLA), and I am providing my contributions under its terms.
Note
Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/open-webui/open-webui/pull/23736
**Author:** [@Classic298](https://github.com/Classic298)
**Created:** 4/14/2026
**Status:** 🔄 Open
**Base:** `dev` ← **Head:** `feat/stream-resume-redis`
---
### 📝 Commits (10+)
- [`8289ac7`](https://github.com/open-webui/open-webui/commit/8289ac7de3b9602adae29759df6da51feeef4170) feat(stream): resumable WS streaming via Redis log with seq-based replay
- [`ff48c99`](https://github.com/open-webui/open-webui/commit/ff48c99d6c88e50e5655750c2dfdbe6bc6f58e1b) fix(stream): address code review findings on resume-stream
- [`dec9176`](https://github.com/open-webui/open-webui/commit/dec91764edb4fa5f7f8d01d002e763a868b71fc5) fix(stream): resume every in-flight assistant message, not just the current
- [`7dc4d84`](https://github.com/open-webui/open-webui/commit/7dc4d843faa067e58fc33364d57ecf54b6c96cea) fix(stream): harden done-detection and isolate resume seq from persisted state
- [`860bde8`](https://github.com/open-webui/open-webui/commit/860bde88427e5d686e4dcbcd6cf4b4913c938f23) fix(stream): close replay race, cursor-efficient resume, bounded client bookkeeping
- [`d9e2ffc`](https://github.com/open-webui/open-webui/commit/d9e2ffc525951102c0dc09d4f4c22df9e69c2347) fix(stream): truncate stale resume log at emitter creation
- [`46d54c6`](https://github.com/open-webui/open-webui/commit/46d54c6557be3382793ffe213fada9540e2c4612) fix(stream): replace delayed-truncate task with TTL shortening on done
- [`5533368`](https://github.com/open-webui/open-webui/commit/55333686993cb9c4a78df6925933c42270e770f4) style+fix(stream): tighten comments and address two review findings
- [`21ef755`](https://github.com/open-webui/open-webui/commit/21ef7557cdb39fb8c5fd1a7ab30cb7588bcaa926) fix(stream): close replay/live race and drop taskIds gate
- [`d655395`](https://github.com/open-webui/open-webui/commit/d655395b094558f320f9467a54a5f6318147d597) fix(stream): unconditional completion, fence timeout, single-batch replay
### 📊 Changes
**2 files changed** (+567 additions, -5 deletions)
<details>
<summary>View changed files</summary>
📝 `backend/open_webui/socket/main.py` (+347 -5)
📝 `src/lib/components/chat/Chat.svelte` (+220 -0)
</details>
### 📄 Description
# This PR SHOULDNT BE MERGED WITHOUT ALSO MERGING THIS https://github.com/open-webui/open-webui/pull/23859
Adds a bounded Redis stream log for every in-flight assistant message so clients can reconnect mid-stream (page refresh, network drop, device switch) and catch up on frames they missed without re-fetching the full chat from the database.
Problem this solves
-------------------
With ENABLE_REALTIME_CHAT_SAVE=False (default), the backend does not write the assistant message to the DB until the stream finishes. If the client refreshes the page mid-stream, chat load from the DB returns nothing for the in-progress message and the response appears to vanish until the stream eventually completes. Users staring at an empty chat while the backend quietly keeps emitting tokens into the void.
Design
------
* Every outbound WS envelope gets stamped with a monotonic per-message `seq` inside `get_event_emitter` and appended to a bounded Redis stream keyed `{REDIS_KEY_PREFIX}:stream:{message_id}`. MAXLEN ~ 2000 entries, TTL 1h as a safety net.
* Clients track `message.lastSeq` in Chat.svelte as events arrive. On chat load (mid-stream refresh) and on socket reconnect they emit `resume-stream {chat_id, message_id, last_seq}`.
* The server authenticates the session, verifies the user owns the chat, XRANGEs the log, filters by `seq > last_seq`, and emits the missed envelopes to THAT session only (via `to=sid`) so live listeners in the user room keep receiving their normal live stream unchanged.
* The existing chat event handler drops any envelope with `seq <= message.lastSeq`, making replay idempotent against live frames that race the replay after a reconnect.
* When an event with `done: True` fires, a background task truncates the log after a 30s grace window so late reconnects still catch the finalization; anything beyond that resumes from the now-up-to-date DB.
Orthogonality
-------------
Zero touches to middleware.py or the streaming hot path. The log stores whatever gets emitted; any future change to emit shape (chat:message:delta, per-block ops, JSON Patch, ...) is logged and replayed verbatim with no coupling.
Graceful degradation
--------------------
No-op when Redis is not configured (WEBSOCKET_MANAGER != 'redis'). In that deployment mode, refresh during streaming retains the current behavior of waiting for the stream to complete.
Auth model
----------
The log is keyed by message_id only. The resume handler must do a chat ownership check (Chats.get_chat_by_id_and_user_id) before replaying, so a malicious client cannot read another user's stream by guessing a message_id.
### Contributor License Agreement
<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.
Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->
- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/23736
Author: @Classic298
Created: 4/14/2026
Status: 🔄 Open
Base:
dev← Head:feat/stream-resume-redis📝 Commits (10+)
8289ac7feat(stream): resumable WS streaming via Redis log with seq-based replayff48c99fix(stream): address code review findings on resume-streamdec9176fix(stream): resume every in-flight assistant message, not just the current7dc4d84fix(stream): harden done-detection and isolate resume seq from persisted state860bde8fix(stream): close replay race, cursor-efficient resume, bounded client bookkeepingd9e2ffcfix(stream): truncate stale resume log at emitter creation46d54c6fix(stream): replace delayed-truncate task with TTL shortening on done5533368style+fix(stream): tighten comments and address two review findings21ef755fix(stream): close replay/live race and drop taskIds gated655395fix(stream): unconditional completion, fence timeout, single-batch replay📊 Changes
2 files changed (+567 additions, -5 deletions)
View changed files
📝
backend/open_webui/socket/main.py(+347 -5)📝
src/lib/components/chat/Chat.svelte(+220 -0)📄 Description
This PR SHOULDNT BE MERGED WITHOUT ALSO MERGING THIS https://github.com/open-webui/open-webui/pull/23859
Adds a bounded Redis stream log for every in-flight assistant message so clients can reconnect mid-stream (page refresh, network drop, device switch) and catch up on frames they missed without re-fetching the full chat from the database.
Problem this solves
With ENABLE_REALTIME_CHAT_SAVE=False (default), the backend does not write the assistant message to the DB until the stream finishes. If the client refreshes the page mid-stream, chat load from the DB returns nothing for the in-progress message and the response appears to vanish until the stream eventually completes. Users staring at an empty chat while the backend quietly keeps emitting tokens into the void.
Design
seqinsideget_event_emitterand appended to a bounded Redis stream keyed{REDIS_KEY_PREFIX}:stream:{message_id}. MAXLEN ~ 2000 entries, TTL 1h as a safety net.message.lastSeqin Chat.svelte as events arrive. On chat load (mid-stream refresh) and on socket reconnect they emitresume-stream {chat_id, message_id, last_seq}.seq > last_seq, and emits the missed envelopes to THAT session only (viato=sid) so live listeners in the user room keep receiving their normal live stream unchanged.seq <= message.lastSeq, making replay idempotent against live frames that race the replay after a reconnect.done: Truefires, a background task truncates the log after a 30s grace window so late reconnects still catch the finalization; anything beyond that resumes from the now-up-to-date DB.Orthogonality
Zero touches to middleware.py or the streaming hot path. The log stores whatever gets emitted; any future change to emit shape (chat:message:delta, per-block ops, JSON Patch, ...) is logged and replayed verbatim with no coupling.
Graceful degradation
No-op when Redis is not configured (WEBSOCKET_MANAGER != 'redis'). In that deployment mode, refresh during streaming retains the current behavior of waiting for the stream to complete.
Auth model
The log is keyed by message_id only. The resume handler must do a chat ownership check (Chats.get_chat_by_id_and_user_id) before replaying, so a malicious client cannot read another user's stream by guessing a message_id.
Contributor License Agreement
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.