PSYNC_S2305 error, this page shows how to find the cause and bring the count down. For how buckets are counted in the first place, see Bucket Count.
PowerSync enforces two limits per user, both with a default of 1,000. One is the number of unique buckets. The other is the number of parameter query results, counted before duplicates are removed. Exceeding either fails the sync with a PSYNC_S2305 error. The fix is different for each, so start by finding out which one you hit from the error message. See Limits for the full difference.
Diagnosing High Bucket Count
Reading the Error Message First
ThePSYNC_S2305 message tells you which limit you reached. The fix is different for each, so read it first.
Too many bucketsmeans you reached the bucket limit. Reduce the number of unique buckets. Any strategy below helps.Too many parameter query resultsmeans you reached the parameter limit. Reduce the rows your parameter lookups return. Only some strategies help here: Denormalizing the Scope Key and Querying the Membership Table Directly cut the lookups themselves, so they lower both counts.
The Contributor Breakdown
ThePSYNC_S2305 log includes a breakdown of the streams that contribute the most.
- For a bucket-limit error, it lists streams by bucket count, highest first.
- For a parameter-limit error, it lists the streams that returned the most rows, and then the stream that exceeded the limit. Each listed stream shows how many rows it returned. The failing stream instead shows how much budget was left when it failed.
Checkpoint Logs
Checkpoint logs record the counts for each connection. Find them in your instance logs. For example:bucketsis the number of unique buckets for this connection.param_resultsis the total number of parameter rows for this connection.- The array lists the bucket names. Each name already includes its parameter value. The list stops after 20 names.
Sync Diagnostics Client
The Sync Diagnostics Client shows the buckets for one user. It does not load for a user who is over the limit, because that user’s sync fails before the data loads. Use the instance logs and the error breakdown for those users. The client shows the bucket count, which may not be the limit you reached. Confirm the limit from the error message.The Example App
Every example on this page uses the same app. It has organizations, projects, tasks, assets, and documents. Here is how the tables relate:- A user belongs to one or more organizations. The
org_membershiptable links auser_idto anorg_id. - Each project belongs to one organization through
org_id. Each project also has a plain textregioncolumn such asus-east. - Each task belongs to one project through
project_id. - Projects and assets link through the
project_assetstable. - Each document has an
owner_id, ashared_withuser, and anorg_id. - The
categoriestable is shared reference data. It has no owner and no links to the other tables, so it does not appear in the diagram above.
Reducing Bucket Count
Start with the strategy that matches your query pattern. Most high counts come from hierarchical or many-to-many data, where denormalizing the scope key gives the biggest reduction.Multiple Queries per Stream
Reduces: bucket count. Usequeries instead of separate streams to group related tables. All queries in a stream that filter the same way share one bucket per value. See multiple queries per stream.
Before: 5 separate streams, each with a direct auth.user_id() filter, create 5 buckets per user.
After: 1 stream with 5 queries creates 1 bucket per user.
Denormalizing the Scope Key
Reduces: bucket count and parameter query results. This is the most effective fix for parent-child data. When chained queries through org → project → task create too many buckets, filter every table with the same top-level parameter, such asorg_id. A bucket’s key must be a column on the table you sync (see The Partition Key Must Exist on the Row below). So this works only if the child tables have that column. If tasks only have project_id, add org_id to the tasks table.
Before: chained queries create 10 + 500 = 510 buckets for 10 orgs with 50 projects each. Projects and tasks share buckets because they use the same filter. Orgs use a different filter, so they add their own buckets.
After: add org_id to the tasks table, drop the user_projects CTE, and filter every table by org. This creates 10 buckets.
Querying the Membership Table Directly
Reduces: bucket count and parameter query results. When a subquery or JOIN through a membership table creates N buckets, query the membership table directly with a direct auth filter. Use no subquery and no JOIN. You often need fields from the related table, such as the org name, alongside each membership row. Denormalize those fields onto the membership table so they are available without a JOIN. Before: N org memberships create N buckets. After: 1 bucket per user, with org fields denormalized ontoorg_membership.
Many-to-Many via a JSON Array Column
Reduces: bucket count. A join through a link table creates one bucket per row of the table you select from. For assets linked to projects throughproject_assets, you get one bucket per asset.
Add a denormalized project_ids JSON array column to assets, maintained with database triggers. Then use json_each() to traverse it. This lets PowerSync key the bucket by project ID instead of asset ID.
Before: one bucket per asset. 2,000 assets create 2,000 buckets.
After: key by project. 50 projects create 50 buckets.
INNER JOIN user_projects syncs only assets that belong to at least one of the user’s projects. The bucket key is the project ID, so the count matches the number of projects, not assets.
Subscription Parameters for On-Demand Sync
Reduces: bucket count. Buckets are created per active subscription, not from every possible value. Usesubscription.parameter('project_id') so the count is bounded by how many subscriptions the client has active.
Before: a subquery returns all of the user’s projects. 50 projects create 50 buckets.
After: the client subscribes per project on demand. 3 open projects create 3 buckets.
Edge Cases and Gotchas
The Partition Key Must Exist on the Row
A bucket’s key must be a value that physically exists on a row of the table you sync. You cannot split a table into buckets by a column it does not have. This is why denormalizing the scope key onto child tables is the standard fix. If tasks only haveproject_id, you cannot key their buckets by org_id until you add org_id to the tasks table.
Subscription Parameters Choose Buckets, Not Re-Partition Them
A subscription parameter lets the client choose which existing buckets to sync. It does not change how those buckets are defined. For a parameter to select a bucket, its value must match a value on the row being synced. For example, each task has aproject_id, so you can use that column to group tasks into project buckets:
project_id. Passing a project_id as a subscription parameter therefore cannot make PowerSync group those assets by project. The asset row has no project ID to match against.
If you want to sync assets by project, the asset row needs to contain a project reference first. For example, you could add a project_ids array column as described in Reducing Bucket Count.
Correlated Joins Behave Like Subqueries
A correlated JOIN and anIN (subquery) compile to the same internal form. They create the same number of buckets. Rewriting one as the other does not reduce the count.
CTEs Cannot Reference Each Other
Each CTE must be self-contained. A CTE cannot reference another CTE by name. If it does, the deploy fails. Inline the nested subquery instead. See CTE limitations.Global Buckets Multiply Storage and Cost
A stream with no filter creates one global bucket that every user syncs. Underauto_subscribe: true, every write to that table fans out to every user. This drives up synced data volume and cost. Scope global buckets carefully, and only mark truly shared reference data as global.
Bucket Storage Does Not Shrink When You Archive
Buckets are append-only. Marking a row as archived does not remove it from bucket storage on its own. A row leaves storage only when it stops matching the data query, through a hard delete or a filter on the table’s own column. Storage reclaims space during compaction. Filtering through a parent table does not shrink a child table’s stored data.Increasing the Limit
Raise the limit only after you exhaust the reduction strategies above. Before you raise it, weigh the cost. Sync overhead scales roughly linearly with the number of buckets per user. Doubling the bucket count roughly doubles sync latency for a single operation. It also roughly doubles CPU and memory use on the server and the client. Many operations inside a single bucket scale much more efficiently than many buckets. The 1,000 default exists to encourage fewer, larger buckets and to protect the service from excessive counts. On PowerSync Cloud, you can request a higher limit on Team and Enterprise plans, up to 10,000. The limit applies per user, so your instance can still track far more buckets in total. For self-hosted deployments, set the limits underapi.parameters:
service.yaml
Related Pages
- Bucket Count explains how buckets are counted and the two limits.
- Writing Queries covers the query syntax that determines your bucket count.
- Common Table Expressions (CTEs) covers shared filtering logic.
- Troubleshooting covers the
PSYNC_S2305error. - Performance and Limits lists the Service limits.