From af23d4e272c0ae3dcf9405b2b56c770b49893879 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:50:49 +0200 Subject: [PATCH] perf: stop rescanning the whole chat JSON on every streamed event write Every streamed event that persists to a chat (status updates, citations, file attachments, message content) serialized the entire conversation JSON three times: a null-byte check of the stored row, a second sanitize of the whole blob after merging in the event payload and the flush of the UPDATE itself. The middle pass rescans megabytes of already-clean history for null bytes that can only come from the small incoming payload, so long chats pay for their full history on every single event. The write paths now sanitize just the incoming message, message id and status dict and keep the row-level sanitize, so legacy rows with null bytes still self-heal as before. Median per-event write time (sqlite, orjson): 1 MB chat 15.0 ms to 11.1 ms, 4 MB 65.2 ms to 52.4 ms, 10 MB 159.9 ms to 124.5 ms, roughly 20 percent less per event. As a side effect the chat_message dual write now receives the sanitized message; previously null bytes in non-content fields were cleaned in the blob but written raw to chat_message, which failed that insert on PostgreSQL. Verified byte-identical rows against the previous implementation across nine scenarios covering null bytes in every input, legacy dirty rows, a missing title and a NULL chat column. --- backend/open_webui/models/chats.py | 39 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 0d918729c9..9854af6d3a 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -398,6 +398,9 @@ class ChatTable: """ Clean a Chat SQLAlchemy model's title + chat JSON, and return True if anything changed. + + The message write paths (upsert/status/delete) rely on this + leaving the blob clean and sanitize only the data they add. """ changed = False @@ -1052,9 +1055,8 @@ class ChatTable: if output_text: message['content'] = output_text - # Sanitize message content for null characters before upserting - if isinstance(message.get('content'), str): - message['content'] = sanitize_text_for_db(message['content']) + message = self._clean_null_bytes(message) + message_id = self._clean_null_bytes(message_id) try: async with get_async_db_context() as session: @@ -1069,10 +1071,9 @@ class ChatTable: history = chat.get('history', {}) saved_message = self.upsert_message_to_history(history, message_id, message) chat['history'] = history - clean_chat = self._clean_null_bytes(chat) - chat_item.chat = clean_chat - chat_item.title = self._clean_null_bytes(clean_chat['title']) if 'title' in clean_chat else 'New Chat' - chat_item.current_message_id = self.get_current_message_id(clean_chat) + chat_item.chat = chat # chat is a fresh dict when the column was empty + chat_item.title = chat.get('title', 'New Chat') + chat_item.current_message_id = self.get_current_message_id(chat) flag_modified(chat_item, 'chat') if touch: @@ -1111,22 +1112,18 @@ class ChatTable: history = chat.get('history', {}) deleted_ids = self.delete_message_from_history(history, message_id) if not deleted_ids: - clean_chat = self._clean_null_bytes(chat) - chat_item.chat = clean_chat - chat_item.title = ( - self._clean_null_bytes(clean_chat['title']) if 'title' in clean_chat else 'New Chat' - ) - chat_item.current_message_id = self.get_current_message_id(clean_chat) + chat_item.chat = chat + chat_item.title = chat.get('title', 'New Chat') + chat_item.current_message_id = self.get_current_message_id(chat) flag_modified(chat_item, 'chat') await session.commit() return ChatModel.model_validate(chat_item) messages = history.get('messages') or {} chat['history'] = history - clean_chat = self._clean_null_bytes(chat) - chat_item.chat = clean_chat - chat_item.title = self._clean_null_bytes(clean_chat['title']) if 'title' in clean_chat else 'New Chat' - chat_item.current_message_id = self.get_current_message_id(clean_chat) + chat_item.chat = chat + chat_item.title = chat.get('title', 'New Chat') + chat_item.current_message_id = self.get_current_message_id(chat) flag_modified(chat_item, 'chat') chat_item.updated_at = int(time.time()) await session.commit() @@ -1144,6 +1141,7 @@ class ChatTable: self, id: str, message_id: str, status: dict ) -> ChatModel | None: try: + status = self._clean_null_bytes(status) async with get_async_db_context() as session: chat_item = await session.get(Chat, id) if chat_item is None: @@ -1160,10 +1158,9 @@ class ChatTable: history['messages'][message_id]['statusHistory'] = status_history chat['history'] = history - clean_chat = self._clean_null_bytes(chat) - chat_item.chat = clean_chat - chat_item.title = self._clean_null_bytes(clean_chat['title']) if 'title' in clean_chat else 'New Chat' - chat_item.current_message_id = self.get_current_message_id(clean_chat) + chat_item.chat = chat + chat_item.title = chat.get('title', 'New Chat') + chat_item.current_message_id = self.get_current_message_id(chat) flag_modified(chat_item, 'chat') await session.commit()