Skip to content

Long ranges and limits ​

CQL samples a long range rather than reading it block by block, and a handful of caps keep a run from growing past what can finish.

None of this is a surprise if you know the three numbers involved. This page gives them, explains how CQL picks a step when you do not, and lists what happens when a query is too big.

Why a range is sampled ​

from stETH at 25_800_000..26_000_000 names two hundred thousand blocks, and reading the contract at each of them would be two hundred thousand calls for a chart nobody could read. So a state source has a budget of 10 000 rows per contract, and a range that would exceed it is sampled down to fit.

Events are different: a Transfer stETH emitted is a row whether you like it or not, so an event source is never sampled. It has a cap on the span instead, below.

How the step is chosen ​

If you write every, that is the step and it is never adjusted. If you do not, CQL walks the ladder 1, 2, 5, 10, 20, 50, 100, … and takes the smallest step that keeps the source under 10 000 rows. It reports the choice in the run's diagnostics:

CQL3900 · info · Sampling every 50 blocks to stay under max_state_rows (10000)

Two things are true of any step. The grid is absolute — the sampled blocks are the multiples of the step inside the range, not "every k-th from the start" — so two runs with the same step land on the same blocks. And the end of the range is always included, so the newest value is always present.

Write the step yourself when the ladder's choice is not the one you want to read: every 7200 is close to a day on Ethereum, which is once per rebase for stETH, and every 1h on a time range is exactly an hour.

The caps ​

WhatCapWhen you meet it
State rows per contract10 000Never as an error — the range is sampled instead.
Rows out of any operator100 000CQL3014 before the run when it can be predicted, CQL4004 during it otherwise.
Blocks in one event source10 000 000CQL3015, before the run.
Requests in one run50 000CQL3016 before the run from an estimate, CQL4008 during it.
Result size512 MiBCQL3019 before the run when it can be predicted, CQL4009 after the order, summarize, distinct, join or union that crossed it.
Held in memory at onceabout 8 MiB in the workbenchCQL3019 before the run when it can be predicted, CQL4010 during it.
Columns out of any operator7 812CQL3021, before the run, wherever the query runs.

In the workbench the memory a run holds at once is the byte cap met first: a query whose result would come near 512 MiB is refused long before, as CQL3019 or CQL4010.

A cap that can be seen from the query alone stops the run before anything is fetched, so you do not wait for a query that cannot finish. A cap that depends on the data stops the run at the operator that crossed it and names it. Nothing is ever cut short silently: you get all the rows or a diagnostic, never the first 100 000 with no warning.

Making a query smaller ​

The message always suggests the fix, and it is one of three:

  • Narrow the range. latest-21600..latest instead of latest-720000..latest.
  • Sample more coarsely. every 1d instead of every 1h; stETH moves once a day, so an hourly sample of its share rate is twenty-three copies of the same value.
  • Aggregate. summarize total = sum(e.value) by day = bin($timestamp, 1d) turns a million stETH transfers into a few hundred rows before they reach the grid.

An expand multiplies rows — the staking router's four module ids sampled 5 000 times is 20 000 rows — so a cap you meet after an expand is usually fixed by sampling the source it expands.

A join or a union holds both of its sides at once, so the memory cap you meet there is fixed inside the parentheses as well as before them: summarize or narrow each side before it is joined.

A join also adds every column of its right side to the left's, and an event brings a column for each of its arguments, so a long chain of joins over a wide event can pass the column cap (CQL3021) with no rows at all. project the columns you need on each side before it is joined.

See also ​