clockworklabs/SpacetimeDB · error · DBError
Estimated cardinality
Error message
Estimated cardinality ({estimate} rows) exceeds limit ({limit} rows) What it means
Before executing a batch of queries (typically client subscriptions), the server sums per-query cardinality estimates and compares the total against the database's configured row limit, skipping the check only for auth contexts allowed to exceed it. A batch whose estimate exceeds the limit is rejected up front, protecting the server from unbounded row materialization.
Solutions
- Narrow the queries: add WHERE filters, subscribe to fewer tables, or select only needed rows/columns
- Split the subscription set across multiple clients or requests so each stays under the per-request estimate
- If you administer the server, raise or remove the row limit in the database configuration
- For one-off inspection on self-hosted servers, temporarily raise the limit rather than fighting the estimator
Example fix
// before subscriptionBuilder.subscribe(["SELECT * FROM events"]); // whole table // after subscriptionBuilder.subscribe(["SELECT * FROM events WHERE ts > now() - interval '1 hour'"]);
Defensive patterns
Strategy: validation
Validate before calling
// Client-side: estimate before subscribing and narrow if too broad
const est = await db.db.connectionOneOff(`SELECT COUNT(*) FROM ${table} ${whereClause}`);
if (Number(est) > ROW_LIMIT) throw new Error(`query would exceed row limit; add filters`); Prevention
- Avoid SELECT * subscriptions on unbounded tables; always filter or paginate by key/time
- Know the row limit of your tier/database and size subscription sets against COUNT(*) baselines
- Grow subscriptions incrementally and load-test so estimates stay under the cap as data grows
When it happens
Trigger: Submitting subscriptions (e.g. SELECT * FROM big_table, or multi-table subscription sets) whose combined estimated row count exceeds the row limit configured for the database; ad-hoc SQL on a row-limited database without an exempt auth context.
Common situations: Hosted/free-tier row caps; subscribing SELECT * to growing tables; incrementally adding tables to a subscription until the estimate crosses the cap.
Related errors
- Could not determine return type during RLS resolution
- Failed to determine TableId for query
- Subscriptions must be SQL strings or typed queries
- Subscriptions must have at least one query
- Aborted
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/704e740edfb157eb.
Report an issue: GitHub.
Appendix: source
Thrown at crates/core/src/estimation.rs:27
/// If the caller is not allowed to exceed the row limit,
/// reject the request if the estimated cardinality exceeds the limit.
pub fn check_row_limit<Query>(
queries: &[Query],
db: &RelationalDB,
tx: &Tx,
row_est: impl Fn(&Query, &Tx) -> u64,
auth: &AuthCtx,
) -> Result<(), DBError> {
if !auth.exceed_row_limit()
&& let Some(limit) = db.row_limit(tx)?
{
let mut estimate: u64 = 0;
for query in queries {
estimate = estimate.saturating_add(row_est(query, tx));
}
if estimate > limit {
return Err(DBError::Other(anyhow::anyhow!(
"Estimated cardinality ({estimate} rows) exceeds limit ({limit} rows)"
)));
}
}
Ok(())
}
/// Use cardinality estimates to predict the total number of rows scanned by a query.
pub fn estimate_rows_scanned(tx: &Tx, plan: &PhysicalPlan) -> u64 {
match plan {
PhysicalPlan::TableScan(..) | PhysicalPlan::IxScan(..) => row_estimate(tx, plan),
PhysicalPlan::Filter(input, _) => estimate_rows_scanned(tx, input).saturating_add(row_estimate(tx, input)),
PhysicalPlan::NLJoin(lhs, rhs) => estimate_rows_scanned(tx, lhs)
.saturating_add(estimate_rows_scanned(tx, rhs))
.saturating_add(row_estimate(tx, lhs).saturating_mul(row_estimate(tx, rhs))),
PhysicalPlan::IxJoin(IxJoin { lhs, unique: true, .. }, _) => {
estimate_rows_scanned(tx, lhs).saturating_add(row_estimate(tx, lhs))
}View on GitHub (pinned to 6dee26c6ef)