Estimated reading time at 200 wpm: 9 minutes
As many will know, I use OpenWebUI for API access to over 400 AI models, of which I use about 10. I didn’t have to think about this topic, until I realised my OWUI system was slowing down, suffering disconnections and errors. I don’t know how the insides of these things work, any more than an average passenger on a plane knows exactly how plane engines work.
Whether or not you agree our Fat Disclaimer applies
OpenWebUI is a popular self-hosted frontend for interacting with large language models via API. It supports multiple backends — including OpenRouter, DeepSeek, Ollama, and OpenAI-compatible endpoints — and stores full conversation histories in a local SQLite database. And I don’t even need to know what that means.
So when things got serious, I did not call a technical helpline. I called Claud.ai. Claude is a genius at all this stuff. So the investigations started by me being given code to enter into PowerShell, then pasting back results to Claude. And so we went, through serval cycles of troubleshooting. It took about an hour. It would have taken days if this was over the phone with some human.
This article documents a fault pattern that is likely affecting thousands of OpenWebUI installations. A single JSON parsing error in one chat thread led to the discovery of widespread invisible corruption across an entire database, caused by a gap in how OpenWebUI handles failed API streaming responses.
The database in question had been in daily use for approximately twelve weeks, contained hundreds of threads, and weighed in at 875 MB.
The presenting symptom
A long-running thread — over 1,200 messages accumulated across several weeks — began throwing a JSON parsing error: Unexpected token 'd', "data: {"id"... is not valid JSON.
The data: {"id"... prefix is Server-Sent Events (SSE) format, the streaming protocol used between OpenWebUI and its API backends. Something in the thread was failing to parse a streamed response as valid JSON.
The thread stalled on this error. Attempting to open it in a new browser tab produced a different error entirely:
Open WebUI Backend RequiredOops! You're using an unsupported method (frontend only).Please serve the WebUI from the backend.
Yes that was the message.
How OpenWebUI stores chat data
Understanding the fix requires understanding where OpenWebUI keeps its data. The application uses a SQLite database (webui.db) stored inside the Docker container at /app/backend/data/. Chat data lives in multiple places simultaneously.
The chat_message table
Each individual message — user and assistant — gets its own row in the chat_message table. Key columns include id, chat_id, role, content, meta, and timestamps. This is the primary message store.
The embedded history blob
The chat table contains a chat column (confusingly named) holding a large JSON blob. This blob includes a history object with a messages dictionary — effectively a second copy of the entire conversation, including parent-child relationships between messages, model assignments, and branching data. In the affected thread, this blob alone was roughly 35 MB.
The tasks column
The chat table also has a tasks column — a JSON array storing task-tracker items that OpenWebUI renders as a widget at the bottom of a conversation. This column is not immediately obvious from the application’s user interface and only becomes visible when inspecting the database schema directly.
Hunting the cause
The investigation required working through each storage layer in sequence. The container did not have sqlite3 installed, so all database queries were executed via Python scripts copied into the container using docker cp and run with docker exec.
Finding the empty shells
The first step was to locate the thread in the database using its chat ID (extracted from the URL) and examine recent messages. The last five messages appeared normal. A broader search for messages containing raw SSE data (data: {) also returned nothing.
The breakthrough came from searching for assistant messages with suspiciously short content. The query:
SELECT id, role, created_at, LENGTH(content), content
FROM chat_message
WHERE chat_id = '<chat_id>'
AND role = 'assistant'
AND LENGTH(content) <= 5
ORDER BY created_at
returned 158 results. All but two had a content length of 2 — meaning the stored content was simply "", an empty string. These were the remains of failed API streaming responses. Each time a response from the backend timed out or dropped mid-stream, OpenWebUI had created the assistant message entry in the database before the response arrived. When the response failed, the empty shell was never cleaned up. It just stayed there.
Two storage layers, same corruption
Deleting the empty messages from chat_message was straightforward:
DELETE FROM chat_message
WHERE chat_id = '<chat_id>'
AND role = 'assistant'
AND LENGTH(content) = 2
AND content = '""'
But the thread still misbehaved after a hard refresh. The same empty messages existed as duplicates inside the embedded history blob in the chat table. These had to be removed separately by parsing the JSON, filtering out assistant entries with empty content, and writing the cleaned blob back:
data = json.loads(chat_blob)
messages = data['history']['messages']empty_keys = [
k for k, v in messages.items()
if v.get('role') == 'assistant' and v.get('content', '') in ['', '""']]for k in empty_keys:
del messages[k]
Parent-child references also needed fixing — each deleted message had to be removed from its parent’s childrenIds list to avoid broken links in the conversation tree.
The stale task that wouldn’t clear
After cleaning both message layers, a persistent task-tracker widget remained at the bottom of the thread, showing an incomplete task in perpetual “in progress” state. The task had originated from a user request that received one of the empty responses — the model never completed the work, but the task entry was never cleared.
Attempts to remove it by deleting the originating message from the history blob failed. Asking the model to mark the task as complete also failed — it reported having tools to create and update tasks, but not to delete them, and could not see the task ID.
A brute-force search across every table and column in the database for the task content finally located it:
SELECT id, title, tasks FROM chat
WHERE tasks IS NOT NULL AND tasks != '[]'
The tasks column on the chat table held a JSON array with the stale entry:
[{"id": "1", "content": "...", "status": "in_progress"}]Setting it to an empty array cleared the widget:
UPDATE chat SET tasks = '[]' WHERE id = '<chat_id>'
The scale of the problem
With the single-thread fix confirmed, a database-wide scan was the obvious next step. A read-only report script was written to search every chat for the same patterns: empty assistant messages in chat_message, empty assistant messages in embedded history blobs, and stale in-progress tasks.
The results:
- 470 empty assistant messages in the
chat_messagetable across 39 chats - 471 empty messages in embedded history across the same threads
- 1 additional stale task in a separate thread
The worst-affected threads had accumulated 79, 74, 63, and 56 empty messages respectively. These were long-running threads connected to backends that had experienced intermittent outages — the more outages, the more empty shells.
This is not a niche problem. OpenWebUI has over 60,000 stars on GitHub. Any installation connecting to an API with intermittent failures — whether that is DeepSeek during its well-documented instability periods, OpenRouter during provider outages, or self-hosted Ollama on constrained hardware will accumulate this same invisible damage. The longer a thread runs, the worse it gets. Users will experience sluggish thread loading and occasional errors without understanding why.
Building a maintenance routine
Two Python scripts were built for ongoing use: one for reporting, one for cleaning. Both are designed to be copied into the running container and executed via docker exec.
The report script
The report script (owui_report.py) opens the database in read-only mode. It scans all three storage locations — chat_message table, embedded history blobs, and the tasks column — and outputs per-chat counts with thread titles and chat IDs. It modifies nothing.
The cleanup script
The cleanup script (owui_cleanup.py) performs three operations:
- Deletes all empty assistant messages (content =
"") from thechat_messagetable. - Parses every chat’s embedded history blob, removes empty assistant entries, fixes parent-child references, and writes the cleaned JSON back.
- Clears any stale in-progress tasks from the
taskscolumn.
It outputs a per-chat summary and totals for verification.
The workflow
The maintenance routine follows five steps:
- Back up the database. Copy
webui.dbout of the container to a local folder before touching anything. The backup command usesdocker cp. - Run the report. Review the output. If the database is clean, stop.
- Run the cleanup. Check that the numbers in the output match the report.
- Verify. Run the report again. All counts should be zero.
- Hard refresh. Press Ctrl+Shift+R in the browser to clear cached data.
Restoring from backup is a three-command operation: stop the container, copy the backup file back in, start the container. The backup is a complete copy of the database and restores everything to the exact state it was in when the backup was made.
A monthly cadence is sufficient for most installations. Heavy users connecting to unstable backends may want to run it fortnightly.
What should be fixed upstream
The root cause is a cleanup gap in OpenWebUI’s response handling. When a streaming API response fails — whether from a timeout, a dropped connection, or a malformed payload — OpenWebUI creates the assistant message database entry before the response completes, then does not delete it when the response fails. The empty shell persists indefinitely.
The fix is straightforward: when a streaming response fails or returns no content, delete the pre-created message entry from both the chat_message table and the embedded history blob. This is a few lines of code in the backend’s response handler.
Until that fix lands, the maintenance scripts described above are the workaround.










