From a3ea7bf0433172380e9af4881eed7a1fbcf8f319 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:55:46 +0200 Subject: [PATCH] fix(retrieval): offload Loader.load to a worker thread so file uploads stop blocking the event loop (#23705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loader.load() dispatches to the underlying langchain document loaders (PyMuPDF, Unstructured, python-docx, Tika, …) which are all synchronous and CPU/IO-bound. process_file() awaited it directly on the event loop, so parsing a non-trivial PDF/DOCX would freeze the entire FastAPI app for the duration of the parse — which is what users experience as "the server hangs whenever I upload a file." Add an `aload()` async wrapper on Loader that runs the sync load on a worker thread via asyncio.to_thread, and update process_file() to await it. The sync API is preserved so existing callers that already run inside run_in_threadpool (e.g. save_docs_to_vector_db) are unaffected. https://claude.ai/code/session_01JSr4NZSskEUQvoJnavVXh8 Co-authored-by: Claude --- backend/open_webui/retrieval/loaders/main.py | 13 +++++++++++++ backend/open_webui/routers/retrieval.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index 57867d78f5..7dc9df37ce 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -1,3 +1,4 @@ +import asyncio import requests import logging import ftfy @@ -238,6 +239,18 @@ class Loader: return [Document(page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata) for doc in docs] + async def aload(self, filename: str, file_content_type: str, file_path: str) -> list[Document]: + """ + Async wrapper around `load`. + + Document loaders dispatched by `_get_loader` (PyMuPDF, Unstructured, + python-docx, Tika, etc.) are uniformly synchronous and CPU/IO-bound. + Calling `load` directly from an async handler would block the event + loop for the entire parse — minutes for large PDFs. This offloads + the work to a worker thread so the loop stays responsive. + """ + return await asyncio.to_thread(self.load, filename, file_content_type, file_path) + def _is_text_file(self, file_ext: str, file_content_type: str) -> bool: return file_ext in known_source_ext or ( file_content_type diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index c0295ff316..c4f6614adc 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1646,7 +1646,7 @@ async def process_file( MINERU_API_TIMEOUT=request.app.state.config.MINERU_API_TIMEOUT, MINERU_PARAMS=request.app.state.config.MINERU_PARAMS, ) - docs = loader.load(file.filename, file.meta.get('content_type'), file_path) + docs = await loader.aload(file.filename, file.meta.get('content_type'), file_path) docs = [ Document(