From e1e31973f0bfd04dc4195c896eff3476678d2005 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:21:47 +0200 Subject: [PATCH] perf: renaming a folder or deleting all chats takes seconds when many users are online (#32888) * perf: renaming a folder or deleting all chats takes seconds when many users are online Renaming, sharing, moving or deleting a folder, and deleting all chats, each made the server recheck access for every chat that any online user had open in a browser tab, one database lookup at a time, before the request finished, and every one of those open chats reloaded its contents afterwards. Any user can do these, so with a few thousand people online every such click took several seconds and reloaded everyone's open chat. Owners keep access to their own chats (a role change or account deletion already disconnects them), so the recheck now skips owners entirely, with no lookup and no reload, and only checks people viewing someone else's chat and chats that no longer exist. Anyone who lost access is still removed from the chat as before. Postgres 16, one open chat per online user, 2% of them viewing someone else's chat, time for a folder rename (the other actions above run the same recheck): | Online users | No Redis, before | No Redis, after | Redis, before | Redis, after | | --- | --- | --- | --- | --- | | 1,000 | 2.2 s | 0.12 s | 2.5 s | 0.23 s | | 3,000 | 6.9 s | 0.60 s | 7.7 s | 0.78 s | * refac: collect the open chats with a plain loop Same behaviour, the owner lookup now reads more plainly. * refac: shorten the owner comment in the chat access sweep --- backend/open_webui/models/chats.py | 7 +++++++ backend/open_webui/socket/main.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 1e5c60c5b4..901e115961 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -1793,6 +1793,13 @@ class ChatTable: result = await session.execute(stmt) return [meta for meta in result.scalars().all() if isinstance(meta, dict)] + async def get_owner_ids_by_chat_ids(self, chat_ids: list[str], db: AsyncSession | None = None) -> dict[str, str]: + if not chat_ids: + return {} + async with get_async_db_context(db) as session: + result = await session.execute(select(Chat.id, Chat.user_id).filter(Chat.id.in_(chat_ids))) + return dict(result.all()) + async def get_chats_by_model_id( self, model_id: str, diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index a535fb0f51..c8d4641745 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -622,12 +622,23 @@ async def refresh_chat_access(chat_id=None): access = {} chat_ids = set() for batch in get_session_pool_batches(): + chat_owner_ids: dict[str, str] = {} + if not chat_id: + # Owners keep access; role changes and deletion disconnect them separately. + open_chat_ids: set[str] = set() + for _, session in batch: + if not session: + continue + open_chat_ids.update(session.get('chat_ids') or []) + chat_owner_ids = await Chats.get_owner_ids_by_chat_ids(list(open_chat_ids)) for sid, session in batch: if not session: continue for cid in session.get('chat_ids') or []: if chat_id and cid != chat_id: continue + if chat_owner_ids.get(cid) == session['id']: + continue key = (cid, session['id']) if key not in access: user = await Users.get_user_by_id(session['id'])