Replies: 5 comments
|
Some notes on the approach I used to fix each of these, in case it's useful before I open PRs:
All of this keeps the exact same response shape/permission semantics as before (verified by reading through the access-control logic for each affected endpoint) — it's purely about how many round trips it takes to produce the same result. I have this implemented and passing against a synthetic dataset sized to match the production scenario described above (a few thousand users, dozens of organizations, thousands of shared collections in the largest ones), with measured before/after query counts for each point. Let me know if it'd be useful to open this as PRs against |
|
This looks like an AI generated report to me. Also, so items mentioned might cause incompatibility with other databases we support and we need to keep it compatible with all and not make everything over complex. Since this isn't a bug which causes stuff to not work i moved it to ideas. |
|
Yes, this analysis was AI-assisted/generated. English is not my native language (I'm Spanish), so I used AI to help me structure and express the technical findings more clearly. However, the analysis itself is based on a real production instance. The instance is running Vaultwarden 1.37.1 / web-vault 2026.6.4 and has 2,267 users and 130,133 records, with a very large number of organizations and shared collections. This is not a theoretical analysis or something I found by simply asking an AI about Vaultwarden. We are actually seeing these performance problems in production, particularly in environments with a large number of users and collections. I used database-level investigation, query analysis and If any particular finding is questionable, I can provide additional anonymized My main goal with this discussion is simply to bring attention to scalability problems that become very visible when Vaultwarden is used with large organizations and many collections. |
|
Just to clarify the intention behind this discussion: I'm not suggesting that Vaultwarden needs to be completely redesigned or that all of these points necessarily need to be changed. We are active Vaultwarden users, and these findings come from problems we are actually encountering in a production environment with a large number of users and collections. The goal is simply to share these real-world findings and, where possible, contribute fixes or ideas that could help Vaultwarden scale better for organizations with higher usage. If some of these optimizations are not appropriate because of compatibility with other supported databases, that's completely understandable. I mainly want to avoid other organizations operating Vaultwarden at this scale running into the same problems we are seeing today. |
|
Following up on my note above that I could share more evidence if useful, and on the multi-database point @BlackDex raised — here's concrete, fresh data from the fix now running against the real instance described above, not just a synthetic benchmark. All of it is from database-level query monitoring (query text + execution time), and I've kept instance-specific numbers out of it as before. Point 6 (sync Points 4/5 (indexes + CHAR→VARCHAR) — the migration actually ran and stuck. The new indexes and the column-type migration show up as one-time Points 1-3 (N+1 in org/admin screens):
For reference, this is a modest-size organization on this particular instance, so these numbers aren't meant to demonstrate the worst-case gain (that's the synthetic benchmark in the original report) — they're meant to show the new query shapes are real, running in production, and fast in absolute terms, not just relatively faster than a bad baseline. On @BlackDex's multi-database compatibility concern: everything here was investigated and fixed against PostgreSQL specifically, since that's the backend where it was found. Point 5 in particular (the Not trying to relitigate "severe" — just following up with proof that the underlying findings are reproducible and grounded in the real instance mentioned above, not speculative. Happy to keep posting numbers as more of the affected endpoints get hit by real usage, or share more (redacted |
Uh oh!
There was an error while loading. Please reload this page.
Prerequisites
Vaultwarden Support String
Your environment (Generated via diagnostics page)
(Note: reconstructed in the exact format produced by the admin diagnostics page's "Generate Support String" button, using representative values — not a literal capture from a specific instance, to avoid sharing real deployment details. Database version rounded to the minor series.)
Vaultwarden Build Version
v1.37.1
Deployment method
Official Container Image
Custom deployment method
No response
Reverse Proxy
nginx (exact version not relevant to this report — all findings below are server/database-side and reproducible independently of the reverse proxy)
Host/Server Operating System
Linux
Operating System Version
Linux, containerized (e.g. Kubernetes/Docker)
Clients
Client Version
v2026.6.4 (the underlying issue is server/DB-side and not specific to this web-vault version)
Steps To Reproduce
This was found on a self-hosted instance with several thousand users spread across a few dozen organizations, some with a few thousand shared collections and members. Steps to reproduce the most visible symptom:
GET /organizations/{orgId}/collections/details)./admin→ Users, and/admin→ Organizations, on an instance with a few thousand users across dozens of organizations.Expected Result
All three pages should load in well under a second, regardless of organization/instance size, since none of the underlying data changes per request.
Actual Result
/admin→ Users and/admin→ Organizations took over a minute to render on an instance with a few thousand users, to the point of being effectively unusable.POST /api/sync(used by every client on login/refresh) was consistently among the slowest queries in database monitoring, and looking atEXPLAIN ANALYZEshowed a sort/aggregate step spilling to disk under defaultwork_mem.Logs
Not directly relevant — this is a performance/database issue, not a crash or error. Evidence gathered was primarily
EXPLAIN (ANALYZE)output and query-count/timing observations against a real PostgreSQL instance, summarized below.Screenshots or Videos
No response
Additional Context
Root-caused via database-level investigation (query logging,
EXPLAIN ANALYZE, and readingsrc/db/models/*/src/api/core/organizations.rs/src/api/admin.rs) against a production-scale self-hosted instance. Splitting this into distinct findings so they can be triaged/fixed independently; happy to split into separate issues if preferred.1. N+1 queries in
GET /organizations/{orgId}/collections/details(used by the web-vault's "Edit member" and "Collections" screens)For every collection in the organization, the handler issues multiple separate queries (checking the current user's direct/group access to that one collection, that collection's groups, and the current user's membership) instead of loading all of it for the whole organization once. On an organization with a few thousand collections, this turns into thousands of round trips for what is a single page load, roughly:
2. N+1 and per-row aggregate queries in
/admin/usersand/admin/users/overviewFor every user in the instance, the handler separately loads that user's confirmed memberships (and, for each one, the organization row), counts owned ciphers, counts and sums attachments, and looks up the last-active device — instead of computing these once for all users with a handful of
GROUP BYqueries. On an instance with a few thousand users this is thousands of extra queries for a single page load.3. Same pattern in
/admin/organizations/overviewPer organization: separate
count(*)queries againstusers_organizations,ciphers,collections,groups,eventandattachments— six queries per organization instead of six queries total (GROUP BY org_uuid).4. No secondary indexes on the columns most frequently filtered on
The schema only has primary keys and
UNIQUEconstraints; there is no secondary index on any foreign-key column. Unlike MySQL/InnoDB (which indexes FKs automatically), PostgreSQL does not, so every one of these is a sequential scan on tables that can have hundreds of thousands to low millions of rows in a production instance:ciphers.user_uuid,ciphers.organization_uuid,ciphers_collections.collection_uuid,users_collections.collection_uuid,users_organizations.org_uuid,groups.organizations_uuid,groups_users.users_organizations_uuid,collections_groups.groups_uuid,event.org_uuid,devices.user_uuid/devices.refresh_token,attachments.cipher_uuid,folders_ciphers.folder_uuid,favorites.cipher_uuid. This compounds every one of the N+1 patterns above. PR #7678 already proposes adding several of these — see point 5 for why some of them may currently be ineffective on PostgreSQL.5. Inconsistent UUID column types (
CHAR(36)vsVARCHAR) silently defeat indexes on PostgreSQLSome tables/columns use
CHAR(36)(PostgreSQLbpchar) for UUID primary/foreign keys, while others useVARCHAR. Diesel always sends bound parameters astext. Comparing abpcharcolumn against atextparameter forces PostgreSQL to cast the column ((column)::text = $1), which makes the planner unable to use a plain b-tree index on that column — including its own primary key:This means any index proposed on a
CHAR(36)column (including the ones in PR #7678, for the tables where the schema currently usesCHAR(36)) will not actually be used by the queries Vaultwarden itself issues through Diesel, since they all bindtextparameters.6.
SELECT DISTINCTover full wide rows in syncCipher::find_by_user/find_by_user_visible(used by/api/sync, called by every client on login/refresh) computesDISTINCTover all columns ofciphers, including thedatacolumn (the encrypted cipher payload, which can be a few KB per row). Because of the joins to collections/groups, the same cipher can appear multiple times before deduplication, so PostgreSQL has to sort/hash entire wide rows to deduplicate them:On a large organization with many shared collections, this can spend a large fraction of the query's total time in that aggregation step, and under constrained
work_memit can spill to disk (visible asSort Method: external mergeinEXPLAIN ANALYZE, and as growing temp-file I/O in database monitoring).7. Organization events page has no usable index
GET /organizations/{orgId}/eventsfilters and sorts by(org_uuid, event_date); there is no composite index for it (compounded by point 5 ifevent.org_uuidisCHAR(36)in the schema being queried), so it falls back to scanning/sorting a large fraction of theeventtable on every request.8. Write amplification when saving a group/collection with many members
Saving a group or collection triggers one
UPDATE users SET updated_at = ... WHERE uuid = $1per affected user (to bump their sync revision), issued in a loop, instead of a single batchedUPDATE ... WHERE uuid = ANY($1). Saving a group with a few dozen collections and a few dozen users can turn into thousands of individualUPDATEstatements for one save action.I have a working, validated fix for all of the above (schema migration for point 5, new indexes for points 4/7, batched/precomputed queries for points 1-3 and 6, and batched writes for point 8), tested against a synthetic dataset at a scale comparable to the production instance described above. I'm happy to open PRs for these — individually, so each can be reviewed/reverted independently — starting with the index/type fix that's most relevant to PR #7678, if there's interest from the maintainers on the overall direction.
All reactions