mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-18 02:04:06 -05:00
[GH-ISSUE #24868] issue: Performance issue: /api/config triggers heavy SELECT count(*) on every call, consuming high CPU #123726
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @MiXaiLL76 on GitHub (May 18, 2026).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/24868
Originally assigned to: @Classic298 on GitHub.
Check Existing Issues
Installation Method
Docker
Open WebUI Version
v0.9.5
Operating System
Debian
Confirmation
README.md.Expected Behavior
Every time a user authenticates or the frontend requests the /api/config endpoint, it triggers Users.get_num_users(). This method executes a full table count (SELECT count(*) FROM "user"), which causes massive DB load and slow response times on instances with a large number of users.
According to our query statistics, this single query accounts for 70% of total CPU usage:
Code Reference
The endpoint calls get_num_users() here:
https://github.com/open-webui/open-webui/blob/v0.9.5/backend/open_webui/main.py#L2344
The query implementation:
https://github.com/open-webui/open-webui/blob/v0.9.5/backend/open_webui/models/users.py
Python
Generated SQL:
I suggest changing the function to
Actual Behavior
According to our query statistics, this single query accounts for 70% of total CPU usage:
Steps to Reproduce
Login open-web-ui
Logs & Screenshots
@owui-terminator[bot] commented on GitHub (May 18, 2026):
🔍 Related Issues Found
I found some existing issues that might be related. Please check if any of these are duplicates or contain helpful solutions:
🟣 #23939 issue: Loading and login issue after upgrading to 0.9.0 and 0.9.1
This report mentions
/api/configbeing very slow during login and causing the OIDC login flow to hang. It is likely related because the new issue also centers on/api/configperformance impacting authentication, though the specific bottleneck differs.by Joly0 ·
bug🟣 #13724 issue: Admin panel users slow to load for larger user bases
This issue is about user-related admin loading becoming slow as the user base grows, which points to database work scaling poorly with large numbers of users. It is related to the same general performance area and user-count-heavy code path.
by robbiekouwenberg ·
bug🟣 #14945 issue: Performance degradation as active users increase due to "Active Users" count &
user-listemittersThis is a broader performance regression tied to user activity and user-list emissions, showing that user-related backend events can create severe load. It is not the same query, but it is in the same subsystem and illustrates a similar scalability problem.
by taylorwilsdon ·
bug🟣 #19509 issue: User overview page calls /api/v1/users multiple times
This issue reports repeated
/api/v1/usersrequests from the admin UI, which is another case of excessive API/database churn around user data. It is related as a similar frontend/backend inefficiency affecting user-related endpoints.by luke-wren ·
bug🟣 #23793 feat: cache get_user_by_id — 25% of all DB queries with zero caching
This feature request analyzes heavy user lookup queries on authenticated requests and identifies the same general pattern of expensive user DB access on hot paths. While it focuses on
get_user_by_id()rather thanget_num_users(), it is directly relevant to authentication-path query load.by ashm-dev
💡 If your issue is a duplicate, please close it and add any additional details to the existing issue instead.
This comment was generated automatically. React with 👍 if helpful, 👎 if not.
@Classic298 commented on GitHub (May 19, 2026):
investigating
@MiXaiLL76 commented on GitHub (May 19, 2026):
I think I figured out what exactly is complicating the query.
Since selecting the entire COUNT(*) table, rather than by ID, is the problem.
My users have their student information stored in info (json), which is heavy for the database.
If I apply the change above
This will probably fix the problem, but I haven't tested it. I've just made get_num_users() -> return 1000 the default for now.
@Classic298 commented on GitHub (May 19, 2026):
you mean stored in the biography in the profile?
@Classic298 commented on GitHub (May 19, 2026):
@MiXaiLL76 1.7 seconds is too long for a single count operation.
Either your database is slow, or you are running the database over network, or you are running sqlite over slow disk or network attached disk, or you have very frequent user updates. So very often you have
UPDATE "user" SET last_active_at=.The first three are architectural faults of your deployment.
The fourth one is fixable through: https://docs.openwebui.com/reference/env-configuration#database_user_active_status_update_interval and setting it to for example 300.
I will make a PR to replace the user count in the unauthenticated branch with has_users() function which is a much cheaper operation and defer the actual user count from the authenticated branch, but at the end of the day, this will not fix your issue. Your issue is either setup related or caused by the missing env var here. A simple count operation should NEVER EVER take 1.7s on ANY database
@Classic298 commented on GitHub (May 19, 2026):
let us know what type of db you use and what your setup looks like because you didn't share that yet. I'd bet it's a deployment issue
@MiXaiLL76 commented on GitHub (May 19, 2026):
yep, user.info = json{"xxx":"yyy", "zzz" : {1:2} }....
postgresql + REDIS
I simply applied a "patch" to reduce such queries.
And everything started working 100 times faster with my users.
@Classic298 commented on GitHub (May 19, 2026):
PostgreSQL should not take 1.7 seconds to run a count query. Please answer my question re: how is the connection to your PostgreSQL. is it on the same server? is it on a different network machine? What's the latency to that database?
also did you set the env var i recommended?
Finally you can test this PR if you want: https://github.com/open-webui/open-webui/pull/24904
@Classic298 commented on GitHub (May 19, 2026):
Btw i checked and this isn't even being loaded so this is a red herring
the config endpoint runs a simple count query, so the user biography isn't even loaded, or any of the user data. It just counts how many users there are. One of the most simple and fastest queries a database can do.
@MiXaiLL76 commented on GitHub (May 19, 2026):
same k8s node
@MiXaiLL76 commented on GitHub (May 19, 2026):
Overall, calculating using count(*) is a SQL anti-pattern, as I understand it.
I think it's worth considering using count(user.id) instead, as collecting data by index is a much simpler operation.
@Classic298 commented on GitHub (May 19, 2026):
Following up to close the loop on the count(*) vs count(user.id) point, since it'll come up again otherwise.
@MiXaiLL76
count(*)is not an anti-pattern in PostgreSQL. That rule is MySQL/MyISAM folklore and doesn't transfer. In Postgrescount(*)is the planner-optimized form;count(col)is, if anything, slower because it adds a per-row NOT-NULL check. The PG wiki and core devs are explicit thatcount(*)≥ count(1) ≥ count(col).Swapping to count(User.id) would not change anything. Postgres has no stored row count — MVCC means every transaction sees a different number, so any exact count must scan all visible tuples.
count(*)and count(id) produce the same scan and the same plan family. It won't turn 1.7s into milliseconds. This is also why it was never the fix: note you hard-coded return 1000 and never actually benchmarked count(id).Your own result actually disproves the theory. CREATE UNIQUE INDEX CONCURRENTLY idx_user_email making it "100× faster" is the tell: a fresh, compact index let the planner switch to an index-only scan for
count(*)instead of scanning a bloated heap. That path is available tocount(*)too — it has nothing to do with count(id).Why 1.7s mean / 88s max on your userbase? Probably because you aren't scanning tidy rows. A count over a healthy user table of thousand of rows is single-digit milliseconds AT MOST. Those numbers mean the scan is hitting a pathological table — almost certainly dead-tuple + heap/TOAST bloat with autovacuum falling behind. Your own clues line up: frequent UPDATE "user" SET last_active_at=… on wide rows (large user.info JSON). Every UPDATE writes a new row version; with lagging autovacuum the heap balloons and a stale visibility map blocks the cheap index-only scan, forcing a seq-scan over a huge bloated heap. (count never reads info itself, so the JSON isn't loaded — but its width still inflates row size and bloat.)
Quick diagnostics to confirm on your instance:
If n_dead_tup is large relative to n_live_tup, or total/heap is huge for 4k users, that's your 1.7s.
Remediation for the deployment (independent of the code):
VACUUM (FULL, ANALYZE) "user"; once (or pg_repack to avoid the exclusive lock), then
much more aggressive autovacuum on this table, e.g. ALTER TABLE "user" SET (autovacuum_vacuum_scale_factor = 0.02, autovacuum_vacuum_cost_delay = 0);
keep DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL high (you have 180, good)
verify storage isn't a slow network-attached PVC.
The actual fix for the CPU regression is #24904: it stops calling the count on the hot/unauthenticated path entirely (has_users() → SELECT EXISTS, the 335k calls go to ~0) and defers the real count to authenticated admin/user only when a seat-limited license needs it. That resolves the 70% CPU regardless of DB health — but the bloat above is still worth fixing on your side, because it will keep biting other queries.
And as I said, try out this env var also, it will help protect your auto vacuum from falling behind on frequent updates: docs.openwebui.com/reference/env-configuration#database_user_active_status_update_interval