# Indexing Elysium events at ten blocks a second without missing a range

> Elysium produces blocks roughly ten times faster than Ethereum produces seconds of history, and its block rate changes with load. That breaks the habits most EVM indexers carry. This is a design that keeps a complete, ordered event table anyway.

- Canonical: https://triport.io/blog/indexing-elysium-events-at-ten-blocks-a-second
- Published: 2026-11-09
- Updated: 2026-11-09
- Author: Triport Engineering
- Category: Engineering
- Networks: Elysium

## TL;DR

- Elysium's block rate depends on load: over the 100,000 blocks before 9 Nov 2026 00:12 UTC it averaged 10.6 blocks per second, over the last 1,000,000 blocks 6.5. Convert between blocks and time by reading timestamps, never with a constant.
- Backfill with eth_getLogs in windows of at most 10,000 blocks, filtered by address or topics, with a cursor that only advances after a window is stored. A -32602 or -32005 answer means split the window, never treat it as empty.
- Tail with a WebSocket logs subscription, but treat it as a hint: reorged logs are not re-sent, so after every reconnect, and before you mark data final, re-read the gap with eth_getLogs up to the finalized block, which trailed latest by 21,977 blocks (about 47 minutes) at our reading.

Most EVM indexers were written for a chain that makes a block every twelve seconds. A day of Ethereum is about 7,200 blocks, a single `eth_getLogs` call can often cover hours, and a polling loop that wakes once per block has plenty of slack. Elysium, Kinetiq's Arbitrum Orbit L2 on HyperEVM, does not give you that slack. It produces several blocks every second, and the exact rate moves with the load on the sequencer.

This post describes an event indexer that stays complete on Elysium: how to size backfill windows, how to keep a cursor honest, what a WebSocket `logs` subscription does and does not promise, and where finality sits. What the endpoint serves on day one is in [our Elysium launch post](https://triport.io/blog/elysium-mainnet-rpc-on-triport); this one is about using it well. The numbers come from our own reads of Elysium mainnet (chain ID 99901) on 8 and 9 November 2026 and from the limits in our [Elysium documentation](https://triport.io/docs/elysium/limits).

## How fast Elysium actually moves

Kinetiq describes Elysium as a chain with real canonical blocks at a fast cadence and a 300 Mgas/s execution target. What you observe through RPC is a block number that climbs by several units per second, but not at a fixed rate. We read block timestamps over two windows ending at block 2,808,056 on 9 November 2026 at 00:12 UTC:

| Window | Blocks | Seconds | Blocks per second |
|---|---:|---:|---:|
| Last 100,000 blocks | 100,000 | 9,427 | 10.6 |
| Last 1,000,000 blocks | 1,000,000 | 154,459 | 6.5 |

The difference is not noise. A Nitro sequencer creates a block when it has messages to put in one, so a busy hour produces more blocks than a quiet one. Two habits fail because of this. The first is converting time to blocks with a constant: "the last hour is the last 36,000 blocks" is right at one moment and wrong at another. The second is assuming one block per poll. If your loop wakes every second and fetches `latest`, it will skip several heights every time, which is fine for a head display and wrong for anything that must see every block.

The fix for both is the same. Work in block numbers, read the timestamp of the boundary blocks when you need wall-clock time, and never assume adjacency between two `latest` reads.

## Backfill: windows, filters and an honest cursor

Our endpoint accepts `eth_getLogs` over at most 10,000 blocks per request, counting both ends. A wider range returns `-32602` with a message asking you to split it. A response larger than 8 MB returns `-32005`. Neither case returns a partial result, so a backfill that handles both errors correctly cannot silently lose events. At ten blocks per second, 10,000 blocks is about sixteen minutes of chain time; at the slower average it is closer to twenty-six. The [eth_getLogs reference](https://triport.io/docs/elysium/json-rpc/eth_getLogs) has the exact request and response shapes.

A backfill loop that stays correct has four rules.

**Filter at the source.** Pass `address` and, where you can, `topics`. An unfiltered window on a busy stretch of chain can carry thousands of logs and run into the 8 MB cap long before it reaches 10,000 blocks. Filtering also keeps your request rate meaningful: `eth_getLogs` has its own rate category, from one request per second on the trial to ten on Business.

**Advance the cursor only after the write.** Store `last_completed_block` in the same transaction as the logs from the window that ends there. If the process dies between the RPC call and the commit, the window is simply read again. Deduplicate on `(blockHash, logIndex)`, or on `(transactionHash, logIndex)` once the block is final, so a replayed window is harmless.

**Split, do not skip.** On `-32602` or `-32005`, halve the window and retry the first half, then the second. Keep halving down to a single block if you must. A single block that still exceeds the size cap tells you the filter is too broad, which is a configuration problem, not a data gap. Never record an error as "no logs in this range".

**Pin the upper bound.** Read the head once, choose `toBlock` from that number, and leave anything above it for the next pass. Using the literal tag `latest` as `toBlock` makes a window's meaning change between the retry and the original call.

Run in this order: backfill from your start block up to the current finalized block first, store those rows as final, then hand over to the tailing loop described below.

## Tailing: WebSocket logs as a hint, eth_getLogs as the record

For live data, `wss://triport.io/ws/elysium/<API_KEY>` accepts `eth_subscribe` with `logs` from Basic up and `newHeads` from Pro up. A `logs` subscription must carry an address or topics filter. In our two 60-second live runs on 8 November, the `logs` stream delivered 577 of 577 matching logs and `newHeads` delivered every header in order, without gaps. Headers arrive in small groups, about once per second, rather than one frame per block, which matches the block rate above.

Two properties decide how you use the stream.

**Reorged logs are not re-sent.** Our Elysium WebSocket does not emit `removed: true` notifications. If the chain replaced a block after you received its logs, the stream will not tell you. On a single-sequencer rollup such replacements are rare, but an indexer that must be correct cannot assume "rare" means "never".

**A disconnect leaves a hole.** After a reconnect you receive logs from the new subscription onwards only. Whatever happened in between is invisible to the stream.

So the stream is the fast path, not the record. Write streamed logs into a provisional table keyed by block number. On every reconnect, read the gap with `eth_getLogs` from your last confirmed block to the current head, using the same filter. Periodically, and always before promoting rows to final, re-read the range up to the finalized block and replace the provisional rows with what that read returns. Slow consumers are disconnected with close code 1013, which is a signal to process frames off the socket thread, not to reconnect faster.

If you only need new heads to drive polling, `newHeads` gives you block numbers without a polling loop. If your plan does not include it, poll `eth_blockNumber` once a second and fetch the logs of every height between the previous and the current value.

## Where final is

Elysium settles on HyperEVM. A block becomes final for the RPC tag `finalized` after its batch has been posted to HyperEVM and the parent block holding that batch is final, so the tag moves in steps that follow batch posting rather than block production. On 9 November 2026 at 00:12 UTC, `finalized` and `safe` both pointed at block 2,786,079, which was 21,977 blocks, or 2,842 seconds, behind `latest`. On a quieter day the gap will be shorter, and on a busy one longer.

For an indexer this gives three useful states:

- **seen**: delivered by the stream or read at `latest`, may still change;
- **confirmed**: re-read by `eth_getLogs` and stored, still above `finalized`;
- **final**: at or below the `finalized` block when re-read.

Most applications can show "seen" data immediately and settle balances or payouts at "final". The withdrawal window to HyperEVM is a separate matter, measured in HyperEVM blocks, and it does not apply to events that stay on Elysium. The [bridge guide](https://triport.io/learn/bridge-hype-to-elysium) covers that path.

## Budgeting requests

The arithmetic is simple enough to do before you write code. A backfill of the chain's first 2.8 million blocks with one filter is about 281 windows of 10,000 blocks. At one `eth_getLogs` per second, that is under five minutes of requests, plus any splits. A live tail that re-reads the last finalized gap every five minutes adds a few requests per window. Block reads for timestamps are ordinary reads, at five requests per second on the trial and up to forty on Business. JSON-RPC batches are accepted, and each element counts as one request against the rate.

If the job is "every transaction an address ever sent", events are the wrong tool: an EOA transfer emits no log. For that question, see the batch-history product in the docs, which indexes signed transactions from sequencer batches since genesis.

## When this is the wrong approach

- **You need every transaction of an address, not every event of a contract.** Logs miss plain value transfers and calls that emit nothing. Use [Elysium batch history](https://triport.io/docs/elysium/das) for signed transactions since genesis, and receipts for execution results.
- **You need execution traces or internal calls.** `debug_*` and `trace_*` are not available on Elysium mainnet nodes, so no endpoint can serve them; an event-only design cannot reconstruct internal value transfers.
- **You need sub-second reaction to pending transactions.** Elysium has no public mempool and no `newPendingTransactions`; the earliest public signal is the block.
- **You only display a head number.** Poll `eth_blockNumber` and skip the indexer entirely.
- **Your data must be final before anyone sees it.** Then skip the stream and index from `finalized` only, accepting the delay shown above.

For the network itself, start at [what Kinetiq Elysium is](https://triport.io/learn/what-is-kinetiq-elysium) and [Elysium's chain IDs](https://triport.io/learn/elysium-chain-id). Endpoints, plans and the keyless public URL are on the [Elysium RPC page](https://triport.io/elysium-rpc).

## Sources

- Block timestamps over the last 100,000 and 1,000,000 blocks, the `finalized` and `safe` tags, and the `l1BlockNumber` field: read from Elysium mainnet through triport.io, 2026-11-09 00:12 UTC.
- `eth_getLogs` range, size cap and error codes, and rate categories: [Elysium limits](https://triport.io/docs/elysium/limits) and the [eth_getLogs reference](https://triport.io/docs/elysium/json-rpc/eth_getLogs), checked 2026-11-09.
- WebSocket behaviour (logs 577 of 577, newHeads in order, no `removed` flag, close code 1013): Triport live tests through triport.io, 2026-11-08.
- Elysium's architecture and execution target: [Kinetiq docs, what is Elysium](https://elysium.kinetiq.xyz/docs/what-is-elysium), read 2026-11-08.

## Try it

- Reference: [eth_getLogs on Elysium — ranges, size cap and errors](https://triport.io/docs/elysium/json-rpc/eth_getLogs)
- Network: [Elysium RPC endpoint, WebSocket and plans](https://triport.io/elysium-rpc)
