clockworklabs/SpacetimeDB · error

SQL query exceeds maximum allowed length: \"{sql:.120}...\"

Error message

SQL query exceeds maximum allowed length: \"{sql:.120}...\"

What it means

compile_subscription rejects any SQL text longer than 50,000 UTF-8 bytes (MAX_SQL_LENGTH). The cap is a deliberate guard, flagged as a 'DIRTY HACK' in source, against stack overflow when the compiler recurses over queries with deeply nested AND/OR condition trees.

Source

Thrown at crates/query/src/lib.rs:32

use spacetimedb_physical_plan::{
    compile::{compile_dml_plan, compile_select, compile_select_list},
    plan::{ProjectListPlan, ProjectPlan},
};
use spacetimedb_primitives::TableId;
use spacetimedb_schema::table_name::TableName;

/// DIRTY HACK ALERT: Maximum allowed length, in UTF-8 bytes, of SQL queries.
/// Any query longer than this will be rejected.
/// This prevents a stack overflow when compiling queries with deeply-nested `AND` and `OR` conditions.
const MAX_SQL_LENGTH: usize = 50_000;

pub fn compile_subscription(
    sql: &str,
    tx: &impl SchemaView,
    auth: &AuthCtx,
) -> Result<(Vec<ProjectPlan>, TableId, TableName, bool)> {
    if sql.len() > MAX_SQL_LENGTH {
        bail!("SQL query exceeds maximum allowed length: \"{sql:.120}...\"")
    }

    let (plan, mut has_param) = parse_and_type_sub(sql, tx, auth)?;

    let Some(return_id) = plan.return_table_id() else {
        bail!("Failed to determine TableId for query")
    };

    let Some(return_name) = tx.schema_for_table(return_id).map(|schema| schema.table_name.clone()) else {
        bail!("TableId `{return_id}` does not exist")
    };

    // Resolve any RLS filters
    let plan_fragments = resolve_views_for_sub(tx, plan, auth, &mut has_param)?
        .into_iter()
        .map(compile_select)
        .collect::<Vec<_>>();

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Replace long OR/IN chains with a join against a table containing the keys, or filter on a scalar column.
  2. Split the subscription into several smaller subscriptions and merge results client-side.
  3. Persist the filter keys via a reducer into a helper table and subscribe with a join against it.

Example fix

-- before: thousands of OR'd predicates, > 50KB
SELECT * FROM t WHERE id = 1 OR id = 2 OR id = 3 /* ... */;

-- after: keys live in a table, query stays tiny
SELECT t.* FROM t JOIN selected_ids s ON t.id = s.id;
Defensive patterns

Strategy: validation

Validate before calling

// client-side guard before subscribing (limit is 50_000 UTF-8 bytes)
const MAX_SQL_LENGTH = 50_000;
if (new TextEncoder().encode(sql).length > MAX_SQL_LENGTH) {
  throw new Error(`SQL too large (${sql.length} bytes); split or use a helper-table join`);
}

Try / catch

try {
  await db.subscription.build([sql]).subscribe();
} catch (e: any) {
  if (String(e.message).includes("exceeds maximum allowed length")) {
    // split into multiple smaller subscriptions, or join a keys table
  }
}

Prevention

When it happens

Trigger: Calling subscribe with a SQL string over 50,000 bytes, typically machine-generated WHERE clauses with hundreds or thousands of OR'd predicates (big IN-list expansions).

Common situations: Dynamically building a subscription from a large entity/id list; clients concatenating per-key predicates into one query; generated SQL from ORMs or scripts pasted into clients.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/018ca3aba635363b. Report an issue: GitHub.