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

  1. Narrow the queries: add WHERE filters, subscribe to fewer tables, or select only needed rows/columns
  2. Split the subscription set across multiple clients or requests so each stays under the per-request estimate
  3. If you administer the server, raise or remove the row limit in the database configuration
  4. 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

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


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)