Use this guide when a squid works but is slow, consumes unpredictable resources,
or behaves differently after a restart or reorg.
Separate ingestion from processing
Before changing RPC settings or scaling the database, measure where time is
spent. Temporarily replace the body of the
batch handler
with a block counter:
Run the probe over the same block range and in the same environment as the slow
run:
- If throughput improves substantially, optimize transformations and database
access. Start with batch processing
and query optimization.
- If throughput stays similar, investigate the data source, network connection,
RPC rate limits, and RPC provider.
An empty handler still advances processor status. Use a disposable database or
a unique stateSchema, as shown above. Do not replace a production handler and
reuse its processor status.
Compare like with like. A historical range served by the SQD Network and a
near-head range served in real time have different performance characteristics.
Request only data you use
Narrow requests by contract, topic, and block range whenever possible. Disable optional
fields that are not used — this reduces data transfer and the amount of data
held in each batch.
For factory contracts, consider the
two-pass indexing pattern
so that most historical requests can use a known address list.
setFields()
controls the shape of returned items. It does not request transactions, logs,
traces, or state diffs by itself. For example:
Here, include: { transaction: true } requests each matching log’s parent
transaction and adds it to block.transactions. The transaction selector in
setFields() only adds input to those requested transaction items. Request
related collections such as transaction logs, traces, or state diffs explicitly
when they are also required.
Make batch handlers deterministic
Batch boundaries are an implementation detail and can change between runs. The
same block range can arrive as several different batch groupings.
- Do not let results depend on mutable module-level or global state carried from
one batch to the next.
- Persist correctness-critical state in the database, or derive it again from
the current batch and durable data.
- Keep in-memory caches bounded and safe to rebuild after a restart.
- Test important changes with multiple clean syncs and compare the resulting
rows or a stable export.
Batch handlers should also be safe to retry. TypeormDatabase can run a
handler more than once for the same data when a transaction is retried or the
chain reorganizes. See the
typeorm-store transaction notes.
Keep batch work bounded
Processors can receive large batches, and the number of blocks and selected
items per batch varies. Keep memory and CPU use close to linear in the number
of selected items:
- collect IDs in a
Set, then load matching rows with one findBy() call;
- update entities in a
Map, then save them as an array;
- avoid nested scans over the complete batch;
- clear temporary collections at the end of each handler call;
- load test with realistic event density, not only a short quiet range.
Do not replace batching with a database read or write per event. The
batch-processing patterns
show how to load and persist entities in groups.
Run periodic work intentionally
By default, a processor fetches only blocks that contain requested items. Add
includeAllBlocks() when logic must run on every block:
includeAllBlocks()
accepts an optional block range, so limit it when the requirement is historical
only. Fetching every block increases work. If the periodic work performs
multiple EVM state reads, use
Multicall.
Make external side effects reorg-safe
When a reorg replaces unfinalized blocks, a compatible database rolls back SQL
changes made for orphaned blocks and the processor runs the handler again on
the canonical chain. Messages already sent to Kafka, HTTP requests, and other
external effects are not rolled back.
For effects derived from unfinalized blocks:
- include block height and block hash in the external record;
- make writes idempotent;
- let consumers replace or retract provisional data when the canonical hash at
a height changes;
- delay irreversible actions until the required confirmation or finality
threshold has been reached.
The unfinalized blocks and reorgs guide
explains which blocks are considered hot and how database rollback works.
Debug a processor near the failure
Use a narrow debug namespace before enabling all logs:
Debug output can be large. If the issue occurs near the chain head, first sync
close to the head with the normal log level, then restart with debug logging for
a short reproduction. See
overriding the log level
and Cloud logging.
For slow or oversized GraphQL requests, use the
query optimization procedure, add
schema indexes, paginate
collections, and configure
OpenReader DoS protection.
Common errors
Processor errors
Error: data out-of-bounds ethers errors on EVM
-
Usually this means that you’re using the decoder on the wrong data. Make sure that the decoder receives only the data you intend it to.
Example: suppose you want to add the processing of a
Mint event to a squid that is currently processing only Transfer events. You change the processor configuration to get the Mint events for you, but you forget to sort the events in the batch handler and a data item with a Mint event finds its way into a decoder of Transfers.
-
Another common source of this error is faulty RPC endpoints. If your EVM processor crashes during RPC ingestion on a log with
'0x' in its data field, try switching to another RPC provider or, if you are developing locally, to another Ethereum network emulator.
BAD_DATA when using a Multicall contract
This error can occur for a variety of reasons, but one common cause is choosing a Multicall deployment that is newer than the oldest blocks that have to be indexed. When batch state queries are performed on historical chain state older than the Multicall deployment, EVM detects that and refuses to run.
Solutions:
- Use an older Multicall deployment.
- Delay your chain state queries until a later block.
These issues are explored in Part 4 of the BAYC tutorial.
Database errors
QueryFailedError: relation "..." does not exist
It is likely that the generated migrations in the db/migrations folder are outdated and do not match the schema file.
Recreate the migrations from scratch as detailed on this page. The relevant commands are listed in the squid-typeorm-migration reference.
If your squid saves its data to a database, all operations with ctx.store are asynchronous. Make sure you await on all store operations like upsert, update, find, save etc.
You may find the require-await eslint rule to be helpful.
QueryFailedError: invalid byte sequence for encoding "UTF8": 0x00
PostgreSQL doesn’t support storing NULL (\0x00) characters in text fields. Usually the error occurs when a raw bytes string (like UIntArray or Bytes) is inserted into a String field. If this is the case, use hex encoding, e.g. using util-internal-hex library. For addresses, use appropriate encoding for your chain type.
GraphQL errors
response might exceed the size limit
Make sure the input query has limits set or the entities are decorated with @cardinality. We recommend using XXXConnection queries for pagination. For configuring limits and max response sizes, see DoS protection.