Normally POST /chunk broadcasts a chunk to the Arweave network and discards the
bytes locally — so if that data is later read or unbundled, the gateway re-fetches
it over the network. With the optimistic chunk ingest cache enabled, the gateway
also validates and write-through caches each posted chunk, so data posted
through the gateway (e.g. by a bundler seeding its chunks) is immediately
available locally.
Disabled by default (CHUNK_INGEST_CACHE_ENABLED=false).
This feature caches chunk bytes only. Two related optimistic mechanisms are separate:
- Optimistic data-item indexing — makes data items queryable by id before
their bundle mines (
POST /ar-io/admin/queue-data-item, already available). - Optimistic L1 transaction indexing — id-resolvability of a signed tx/bundle
before it mines — has since shipped (see
madr/004-optimistic-l1-tx-indexing.md), with a serving guard that withholds theverifiedstamp until the tx mines.
So today the behavior is: chunks cache when posted; when their transaction mines
and is indexed they confirm and are served (and the unbundler reuses the cached
bytes instead of re-fetching). Pre-mine byte-serving of /raw/<id> from this
cache stays deferred (MADR 004 Scope 2), so a pending placement is cached but not
served until its tx mines.
| Variable | Default | Purpose |
|---|---|---|
CHUNK_INGEST_CACHE_ENABLED |
false |
Master switch. |
CHUNK_INGEST_CACHE_ALLOWLIST |
"" (open) |
IPs/CIDRs whose posted chunks are cached. Empty = open ingest; set = only these posters earn caching (gates caching, not posting). |
CHUNK_INGEST_CONFIRMATION_TIMEOUT_SECONDS |
21600 (6h) |
GC leash for open-ingest chunks whose data_root never confirms. |
CHUNK_INGEST_ALLOWLIST_CONFIRMATION_TIMEOUT_SECONDS |
86400 (24h) |
Longer GC leash for allowlisted posters. |
CHUNK_INGEST_MAX_PENDING_BYTES |
26843545600 (25 GiB) |
Runaway-disk backstop; oldest pending evicted first above this. 0 disables. |
CHUNK_INGEST_GC_INTERVAL_MS |
300000 (5m) |
GC sweep interval. |
CHUNK_INGEST_GC_BATCH_SIZE |
1000 |
Max evictions per sweep. |
CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS |
3600 (1h) |
How long a confirmed_data_roots marker is kept before the GC prunes it. Only needs to bridge the confirm→seed gap. |
For a bundler integration, set CHUNK_INGEST_CACHE_ALLOWLIST to the bundler's
egress IP/CIDR and point the bundler's chunk-post endpoint at this gateway.
-
POST /chunk→validateChunk:sha256(chunk)must match thedata_pathleaf, and the merkle proof must validate against the asserteddata_root. An invalid chunk is rejected and not cached. -
Valid → the bytes are written to the chunk store and a pending row is recorded in
chunks.db→chunk_placements(confirmed_atNULL). This is fire-and-forget and never blocks the broadcast response. If thedata_rootis already confirmed (marker present, or a confirmed sibling exists), the new row inherits itsconfirmed_aton the spot (see "sticky confirmation" below). -
When a transaction with that
data_rootis indexed, theTX_INDEXEDevent setsconfirmed_atand recordschunk_ingest_confirmation_latency_seconds. This is a one-shot update: it only touches placements that already exist at that instant. Sticky confirmation carries the result forward. The same event records thedata_rootinconfirmed_data_roots(unconditionally — see below), and that marker makes confirmation persistent:saveChunkPlacementinheritsconfirmed_atfrom the marker, so every chunk ingested after the confirm event self-confirms at ingest; and- the GC TTL sweep skips any
data_rootinconfirmed_data_roots, so a confirmed bundle is never partially evicted regardless of per-row state.
The marker is written even when no chunk of the bundle has been ingested yet. This is essential, not an optimization: with the bundler's TX-confirmation broadcast gate, chunk seeding only starts after the tx confirms network-wide, and the gateway typically imports that same block first — so the confirm event routinely fires before any chunk arrives. An
EXISTS-gated marker would be skipped in exactly that case, and every later-seeded chunk would stay pending and TTL-evict (observed end-to-end: confirm fired ~2 s before the first chunk, all 1025 chunks of a 256 MB bundle evicted,relative_offset0 lost). Marking unconditionally bridges the race in both orderings.Without sticky confirmation, a large bundle (thousands of chunks streamed in over a window longer than — or entirely after — the one confirm event) leaves most chunks unconfirmed; they then TTL-evict and leave a gappy, unservable set (e.g.
relative_offset0 missing → range streaming from offset 0 misses and the whole bundle fails to serve/unbundle until it re-propagates from peers).The
confirmed_data_rootstable is kept bounded by an age-based prune in the GC sweep (CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS, default 1 h): a marker only needs to outlive the confirm→seed gap (seconds to minutes), after which any ingested chunk already carries its ownconfirmed_at. -
A GC worker evicts placements whose
data_rootnever confirms on-chain (tiered TTL by origin) plus a disk-pressure backstop.
The gateway must never serve or permanently keep data that is not on chain. This is upheld by:
- Validate-on-ingest — only chunks that merkle-validate against their asserted
data_rootare cached, so a real transaction cannot be poisoned (only the genuine bytes validate). - Unaddressable until confirmed — serving resolves an id/offset to a
data_rootthrough the index, which contains only mined transactions; a pending (unconfirmed) chunk therefore cannot be served. - Confirmation-driven cleanup — a placement is kept iff its
data_rootis indexed; never-confirmed placements are evicted. The TTL is just the patience window. verifiedonly after real verification — optimistic data is never labeledverifieduntil the background verification worker checks it post-confirmation.- Open ingest is safe because a junk chunk (fake
data_root) is unaddressable and GC-reclaimed; the allowlist further restricts which posters earn caching.
Optimistic L1 tx indexing (MADR 004) can make a pending tx's
data_rootresolvable, so the verification worker withholdsverifiedwhile the tx is unmined and pre-mine byte-serving from this cache stays deferred — a pending placement is still never served.
Open ingest (empty allowlist) lets any client POST chunks, so disk fill is bounded two ways:
- Synchronous disk cap —
CHUNK_INGEST_MAX_PENDING_BYTES(default 25 GiB) is enforced at ingest: once the pending (unconfirmed) total would exceed it, further caching is rejected immediately (chunk_ingest_cache_total{result="skipped_disk_full"}), not minutes later by the sweep — so a burst cannot overrun the disk. - Confirmation-driven GC — anything that never confirms is evicted after the TTL.
For production, prefer allowlist-only (CHUNK_INGEST_CACHE_ALLOWLIST = your
bundler) so only a trusted poster earns caching. Note the cap bounds disk, not
request rate: POST /chunk still does its normal broadcast/validation work
regardless of caching, so put a rate limiter / auth in front of it before exposing
open ingest on a public endpoint.
Set CHUNK_INGEST_CONFIRMATION_TIMEOUT_SECONDS comfortably above your worst-case
POST→index latency. Measure the real distribution on your deployment from the
chunk_ingest_confirmation_latency_seconds histogram and tune down toward
p99 + margin. Too short only loses cache benefit (re-fetch on read); too long is
bounded by the disk-cap and unserveable.
All at /ar-io/__gateway_metrics:
| Metric | Meaning |
|---|---|
chunk_ingest_cache_total{result} |
Outcome per POST: cached, cached_allowlisted, invalid, skipped_not_allowlisted, skipped_disk_full. |
chunk_ingest_confirmed_total |
Pending placements confirmed by tx-indexed events. |
chunk_ingest_confirmation_latency_seconds |
POST→confirm latency histogram (use to tune the TTLs). |
chunk_ingest_evicted_total{reason} |
GC evictions: ttl, disk_pressure. |
chunk_ingest_pending_bytes |
Estimated bytes held by pending (unconfirmed) placements. Refreshed on the GC sweep, so it can lag writes by up to one sweep interval — use chunk_ingest_cache_total for live confirmation. |
chunk_ingest_confirmed_roots |
Rows in the confirmed_data_roots marker table. Should stay bounded (roughly: confirmed data-txs within CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS); a steadily-growing value means the prune is not keeping up. |
chunk_ingest_confirmed_roots_pruned_total |
Count of markers pruned by the GC sweep. |