This document describes the environment variables that can be used to configure the ar.io node.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| START_HEIGHT | Number or "Infinity" | 0 | Starting block height for node synchronization (0 = start from the beginning). With L1_BAND_IMPORT_ENABLED=true and an empty core.db, leaving it unset starts at the chain tip instead, and the history beneath comes from L1 bands |
| STOP_HEIGHT | Number or "Infinity" | "Infinity" | Stop block height for node synchronization (Infinity = keep syncing until stopped) |
| L1_BAND_IMPORT_ENABLED | Boolean | false | Fill this gateway's L1 history (core.db) from installed parquet-l1 bands while it runs, every L1_BAND_IMPORT_INTERVAL_SECONDS. Writes only heights core.db lacks below what the block importer writes itself, in short transactions, so the gateway keeps indexing and serving. With an empty core.db and no START_HEIGHT, the block importer starts at the chain tip instead of 0. Needs the index-swarm sidecar subscribed to parquet-l1 (tools/index-swarm-setup --subscribe <publisher> --l1 sets both). See docs/cli.md, index-l1-import --fill |
| L1_BAND_IMPORT_BANDS_DIR | String | data/indexes/installed/parquet-l1 | Where the index-swarm sidecar installs parquet-l1 bands, as core sees it |
| L1_BAND_IMPORT_INTERVAL_SECONDS | Number | 600 | Seconds between L1 band import runs, counted from the end of the last; the first run is a minute after core starts. A run with nothing to fill writes nothing to core.db |
| L1_BAND_IMPORT_CACHE_MIB | Number | 512 | SQLite page cache for an L1 band import run, in MiB. The run is a child process of core, so this is memory beside the gateway's own |
| TRUSTED_NODE_URL | String | http://envoy:3000 (compose); https://arweave.net outside Docker |
Arweave node to use for fetching data |
| TRUSTED_NODE_HOST | String | "arweave.net" | Hostname for the trusted Arweave node used by Envoy proxy |
| TRUSTED_NODE_PORT | Number | 443 | Port for the trusted Arweave node used by Envoy proxy |
| TRUSTED_GATEWAY_URL | String | unset | A single trusted gateway URL for retrieving contiguous data. When set, it replaces the TRUSTED_GATEWAYS_URLS default with {"<url>": 1} |
| TRUSTED_GATEWAYS_URLS | String | {"https://turbo-gateway.com": 1, "https://arweave.net": {"priority": 2, "trusted": false}} |
A JSON map of gateway URLs to priority (number) or config object ({"priority": N, "trusted": bool}). Simple number values are implicitly trusted. Default: {"https://turbo-gateway.com": 1, "https://arweave.net": {"priority": 2, "trusted": false}}. When trusted is false, data is only cached if a hash is already known and matches; the hash is never written to the DB as authoritative. Untrusted gateways also receive requests without the ar-io-* provenance query params (provenance is still sent via X-AR-IO-* headers) — required for CDN-fronted gateways such as arweave.net, whose CDN returns 502 on those query params. |
| TRUSTED_GATEWAYS_REQUEST_TIMEOUT_MS | String | "10000" | Connection timeout in milliseconds for trusted gateways (time to receive response headers) |
| GATEWAYS_THROTTLE_BACKOFF_ENABLED | Boolean | true | When a trusted gateway answers 429, skip it until its Retry-After has passed instead of queueing every request for it. Set false to ask every gateway on every request, as before |
| GATEWAYS_THROTTLE_BACKOFF_DEFAULT_MS | Number | 30000 | Cooldown in milliseconds after a 429 that carries no usable Retry-After |
| GATEWAYS_THROTTLE_BACKOFF_MAX_MS | Number | 300000 | Longest cooldown in milliseconds, whatever Retry-After asks for |
| GATEWAY_AGENT_IDLE_SOCKET_TIMEOUT_MS | Number | 50000 | Idle-socket timeout (ms) for the outbound trusted-gateway keep-alive agent. Must be strictly less than the peer gateway's server keep-alive timeout (HTTP_KEEP_ALIVE_TIMEOUT_MS, default 60000) to avoid a keep-alive reuse race where the client reuses a socket the server is simultaneously closing (causes ~8-10s peer-fetch stalls). |
| GATEWAY_SLOW_SOCKET_ACQUISITION_LOG_THRESHOLD_MS | Number | 1000 | Outbound gateway socket acquisitions (time from a request needing a socket to one being assigned) at or above this threshold log a warn. Surfaces keep-alive pool waits / socket-reuse stalls before the request hits the wire. See metrics gateway_socket_acquisition_seconds / gateway_socket_connect_seconds. |
| GATEWAY_MAX_SOCKETS_PER_HOST | Number or JSON | 16 | Max concurrent sockets per trusted (internal peer) gateway host. Requests queue once this many sockets to a host are busy; on a busy node the default can leave inter-gateway requests waiting seconds for a slot (high gateway_socket_acquisition_seconds). Trusted peers are low-risk to raise. Accepts a bare integer (all hosts) or a per-host object with an optional default, e.g. {"http://10.84.0.82:4000":128,"default":64}. |
| GATEWAY_UNTRUSTED_MAX_SOCKETS_PER_HOST | Number or JSON | GATEWAY_MAX_SOCKETS_PER_HOST |
Max concurrent sockets per untrusted gateway host (e.g. arweave.net, CDN-fronted). Set lower than the trusted cap to avoid overwhelming a CDN upstream that can throttle / 502 under high concurrency. Same number-or-per-host-object form; untrusted hosts (e.g. arweave.net) are configured here, not in the trusted var. |
| GATEWAY_MAX_FREE_SOCKETS_PER_HOST | Number or JSON | 4 | Max idle keep-alive sockets kept warm per gateway host. Raise toward the max-sockets value when raising GATEWAY_MAX_SOCKETS_PER_HOST so idle capacity is reused instead of reopening TCP/TLS connections under bursty load. Same number-or-per-host-object form. |
| OUTBOUND_MAX_SOCKETS_PER_HOST | Number | 16 | Max concurrent sockets per origin for the non-data outbound clients (root TX discovery sources, GraphQL fan-out). These are low-volume metadata lookups; the point of pooling is not throughput but avoiding a per-request dns.lookup(), which queues on the libuv threadpool behind filesystem I/O and can time out before a socket is opened. Watch outbound_socket_acquisition_seconds{reused="false"} — a rising share means pooling has stopped working. |
| OUTBOUND_MAX_FREE_SOCKETS_PER_HOST | Number | = OUTBOUND_MAX_SOCKETS_PER_HOST | Max idle keep-alive sockets retained per origin for the non-data outbound clients. Defaults to the active cap on purpose: this bounds idle sockets, so any concurrency above it means the excess are destroyed on idle and reopened next request — and every reopen is a fresh DNS resolution, the cost the pooling exists to avoid. Unlike GATEWAY_MAX_FREE_SOCKETS_PER_HOST (throughput management across many hosts), the objective here is maximizing reuse across a handful of upstreams. |
| TRUSTED_GATEWAYS_SEND_UNTRUSTED_PARAMS | Boolean | false | Kill-switch / rollback lever. When false (default), the ar-io-* provenance query params are omitted for untrusted gateways (trusted: false) — provenance still travels via X-AR-IO-* headers. Set to true to revert to the legacy behavior of sending those query params to every gateway. Leave default unless a specific upstream requires the params and tolerates them. |
| STREAM_STALL_TIMEOUT_MS | String | "30000" | Stall timeout in milliseconds for data streams from gateways and peers. Stream is aborted if no data is received for this duration. Prevents stalled transfers from hanging indefinitely while allowing large, actively-streaming transfers to complete. |
| STREAM_REQUEST_TIMEOUT_MS | String | "900000" | Wall-clock cap (ms) on a single HTTP-response stream from ArIODataSource / GatewaysDataSource. Fires regardless of pause/resume state. Belt-and-suspenders for the backpressure-pause-then-upstream-stall wedge — STREAM_STALL_TIMEOUT_MS is cleared on pause, so an upstream that goes silent while we're paused for backpressure can hang a worker forever. Default 15 minutes is generous enough that legitimately slow transfers (~1.1 MB/s for a 1 GB bundle) complete; tighten for environments where peers are fast or relax for genuinely massive transfers. |
| DATA_IMPORTER_DOWNLOAD_TIMEOUT_MS | String | "1200000" | Wall-clock cap (ms) on the entire DataImporter.download() operation — covers the pre-stream cascade walk (SequentialDataSource iterating through BACKGROUND_RETRIEVAL_ORDER) AND the stream-consumption phase. Enforced via Promise.race between the timeout and the download IIFE, so download() is guaranteed to return when the timer fires regardless of cascade behavior; AbortController.abort() is also called as best-effort cleanup but isn't relied upon for the guarantee (some axios/socket states don't honor abort). Catches the pre-getData wedge where workers block in await getData() waiting for some source to return a stream — STREAM_REQUEST_TIMEOUT_MS cannot reach this state because no stream exists yet. Default 20 minutes is comfortably above STREAM_REQUEST_TIMEOUT_MS so legitimate slow downloads finish first; this fires only as a backstop when the cascade itself hangs. |
| GATEWAYS_RANGE_ACCEPT_200_MAX_OFFSET | Number | 10485760 (10 MiB) | Maximum region.offset (in bytes) for which GatewaysDataSource will accept a full HTTP 200 body in response to a Range request and slice it locally. Some upstreams (e.g., nginx with proxy_cache enabled but no slice module) silently strip Range and return the full body; we accept that and slice in-process, but the prefix [0, region.offset) bytes still cross the wire. Above this threshold the bandwidth cost is too high and the response is rejected so the data source chain falls through to the next priority tier. Tune via the gateway_range_ignored_total metric (label outcome=rejected_offset_too_high). |
| TRUSTED_GATEWAYS_BLOCKED_ORIGINS | String | "" | Comma-separated list of X-AR-IO-Origin header values to block when forwarding to trusted gateways (prevents loops and blocks unwanted sources) |
| TRUSTED_GATEWAYS_BLOCKED_IPS_AND_CIDRS | String | "" | Comma-separated list of IPs and CIDR ranges to block when forwarding to trusted gateways (prevents forwarding requests from specific client IPs) |
| TRUSTED_ARNS_GATEWAY_URL | String | "https://NAME.turbo-gateway.com" | ArNS gateway |
| TRUSTED_ARNS_RESOLVER_HOST_HEADER | String | - | Host header override for ArNS resolver requests; supports __NAME__ placeholder (e.g., __NAME__.ar-io.dev) |
| TURBO_ENDPOINT | String | "https://turbo.ardrive.io" | Turbo endpoint URL for root transaction ID lookups |
| TURBO_REQUEST_TIMEOUT_MS | Number | 10000 | Request timeout in milliseconds for Turbo requests |
| TURBO_REQUEST_RETRY_COUNT | Number | 3 | Number of retries for Turbo requests |
| BUNDLER_URLS | String | unset (advertises the built-in default, https://turbo.ardrive.io/) |
Comma-separated list of bundler service URLs advertised in /ar-io/info endpoint for client service discovery |
| GATEWAYS_ROOT_TX_URLS | JSON Object | { "https://turbo-gateway.com": 1 } |
JSON map of AR.IO gateway URLs and priority weights for root transaction offset discovery via HEAD requests. Lower numbers = higher priority |
| GATEWAYS_ROOT_TX_REQUEST_TIMEOUT_MS | Number | 10000 | Request timeout in milliseconds for gateway HEAD requests |
| GATEWAYS_ROOT_TX_RATE_LIMIT_BURST_SIZE | Number | 5 | Rate limit burst size for gateway HEAD requests |
| GATEWAYS_ROOT_TX_RATE_LIMIT_TOKENS_PER_INTERVAL | Number | 6 | Rate limit tokens per interval for gateway HEAD requests (6 per minute = 1 per 10 seconds) |
| GATEWAYS_ROOT_TX_RATE_LIMIT_INTERVAL | String | "minute" | Rate limit interval for gateway HEAD requests (second/minute/hour/day) |
| PEERS_ROOT_TX_URLS | JSON Object | {} | JSON map of peer AR.IO node URLs and priorities for root transaction offset discovery via GET /ar-io/offsets/:id. Lower numbers = higher priority. Empty by default: the endpoint only exists on nodes running a release that serves it, so peers are opted in explicitly. Prefer this over GATEWAYS_ROOT_TX_URLS where peers support it — a miss here costs the peer one indexed read instead of a full retrieval cascade |
| PEERS_ROOT_TX_REQUEST_TIMEOUT_MS | Number | 2000 | Request timeout in milliseconds for peer offsets lookups. Aggressive by design: the peer answers from its index, so a slow response means an unhealthy peer rather than expensive work |
| PEERS_ROOT_TX_RATE_LIMIT_BURST_SIZE | Number | 100 | Rate limit burst size for peer offsets lookups |
| PEERS_ROOT_TX_RATE_LIMIT_TOKENS_PER_INTERVAL | Number | 600 | Rate limit tokens per interval for peer offsets lookups (600 per minute = 10 per second). Far more permissive than the gateway HEAD limits because the peer answers from local disk (its index and any local CDB64 indexes), not with a retrieval |
| PEERS_ROOT_TX_RATE_LIMIT_INTERVAL | String | "minute" | Rate limit interval for peer offsets lookups (second/minute/hour/day) |
| HYPERBEAM_ENDPOINT | String | "https://arweave.net" | HyperBEAM node URL for offset lookups. Override to use a different HyperBEAM node |
| HYPERBEAM_REQUEST_TIMEOUT_MS | Number | 500 | Request timeout in milliseconds for HyperBEAM offset lookups |
| HYPERBEAM_ROOT_TX_RATE_LIMIT_BURST_SIZE | Number | 5 | Rate limit burst size for HyperBEAM offset lookups |
| HYPERBEAM_ROOT_TX_RATE_LIMIT_TOKENS_PER_INTERVAL | Number | 6 | Rate limit tokens per interval for HyperBEAM offset lookups (6 per minute = 1 per 10 seconds) |
| HYPERBEAM_ROOT_TX_RATE_LIMIT_INTERVAL | String | "minute" | Rate limit interval for HyperBEAM offset lookups (second/minute/hour/day) |
| ROOT_TX_LOOKUP_ORDER | String | "db,gateways,graphql,hyperbeam,cdb" | Comma-separated list of root TX lookup sources in order of priority. Options: 'db' (local database), 'cdb' (CDB64 file index), 'peers' (peer AR.IO nodes via GET /ar-io/offsets/:id), 'gateways' (AR.IO gateways via HEAD /raw/:id), 'hyperbeam' (HyperBEAM offset API), 'turbo' (Turbo API), 'graphql' (GraphQL). CDB64 enabled by default since Release 67. List 'peers' before 'gateways': both read the same numbers from a peer's index, but a 'gateways' miss costs the peer a full retrieval cascade while a 'peers' miss is a single indexed read On a gateway with installed index bands (index-swarm), put cdb right after db (e.g. db,cdb,gateways,graphql): the default asks every network source first. See index-swarm.md |
| GRAPHQL_ROOT_TX_BATCH_ENABLED | Boolean | false | Coalesce concurrent GraphQL root-TX lookups into single transactions(ids: [...]) queries. Each batched request consumes one rate-limiter token instead of one per ID, so a page-load burst of asset lookups no longer starves the bucket. Disabled by default; when off, behavior is unchanged (one query per ID) |
| GRAPHQL_ROOT_TX_BATCH_WINDOW_MS | Number | 20 | Coalescing window: how long a forming batch waits for more IDs before flushing. A batch also flushes eagerly once it reaches the max batch size. Only used when GRAPHQL_ROOT_TX_BATCH_ENABLED=true |
| GRAPHQL_ROOT_TX_BATCH_MAX_SIZE | Number | 100 | Default max IDs per batched query. Arweave GraphQL caps first at 100, so this should not exceed 100 |
| GRAPHQL_ROOT_TX_BATCH_MAX_SIZE_BY_URL | JSON object | {} | Optional per-endpoint max batch size overrides, e.g. {"https://arweave-search.goldsky.com": 9} — some gateways cap the number of IDs per query below GRAPHQL_ROOT_TX_BATCH_MAX_SIZE. Endpoints absent from the map use GRAPHQL_ROOT_TX_BATCH_MAX_SIZE |
| GRAPHQL_ROOT_TX_BATCH_MAX_QUEUE_DEPTH | Number | 1000 | Max IDs that may be waiting (pending + in-flight) before new lookups are shed (resolved as not-found) rather than queued unbounded |
| GRAPHQL_ROOT_TX_BATCH_TOKEN_MAX_WAIT_MS | Number | 5000 | Max time a batch waits to acquire a rate-limiter token before giving up (its IDs then resolve as not-found rather than blocking indefinitely) |
| CHUNK_METADATA_ANCHOR_ENABLED | Boolean | true | Enable the chain-anchored chunk metadata fast path for offset → tx + data_root resolution. HEADs a reference peer's /chunk/{offset}/data, parses the X-Arweave-Chunk-* headers, then cross-checks against the chain (/tx/{id}/offset, /tx/{id}) before returning. Slots between the local DB and the existing tx_path / chain-binary-search fallbacks. Reuses the GATEWAYS_ROOT_TX_URLS peer pool when populated, falling back to the first TRUSTED_GATEWAYS_URLS entry. See #681 |
| CHUNK_METADATA_ANCHOR_REQUEST_TIMEOUT_MS | Number | 5000 | HTTP timeout (ms) for the peer HEAD/range-GET against /chunk/{offset}/data. Tight by design — a slow peer should fail fast and let the composite fall through to the existing path |
| CHUNK_METADATA_ANCHOR_TX_CACHE_SIZE | Number | 1024 | LRU cache size for chain-anchored tx metadata. Each entry is keyed by tx-id and holds the chain-verified bounds; sized for the typical working set of recently-probed txs |
| CHUNK_METADATA_ANCHOR_TX_CACHE_TTL_SECONDS | Number | 300 | LRU TTL (seconds) for chain-anchored tx metadata. Bounds the window over which a single chain cross-check covers repeated chunk probes against the same tx. Values are immutable for stable txs, so TTL is purely an eviction knob, not a correctness one |
| MANIFEST_RESOLUTION_CACHE_SIZE | Number | 5000 | LRU cache size for resolved manifest paths, keyed by (manifest id, path). Values are immutable per manifest transaction, so no TTL is used; caching lets repeat requests for the same manifest path skip re-fetching and re-parsing the manifest body |
| CDB64_ROOT_TX_INDEX_WATCH | Boolean | true | Enable runtime file watching for local CDB64 directories. When true, .cdb files added to or removed from a directory, and index bands installed into or retired from a collection directory, are loaded and unloaded without a restart. Every configured local directory is watched, not just the first |
| CDB64_ROOT_TX_INDEX_SOURCES | String | the three shipped resources/ indexes (see description) |
Comma-separated CDB64 index sources, searched in order: local files, directories, collection directories (one subdirectory per index), Arweave TX IDs, bundle data items (txId:offset:size), HTTP URLs and partitioned manifests. The default is resources/cdb64-root-tx-index-non-ao-non-redstone-with-content-type-to-height-1820000,resources/cdb64-root-tx-index-non-ao-non-redstone-without-content-type-to-height-1820000,resources/cdb64-root-tx-index-ao-to-height-1820000; write it out when adding a source, or it stops being searched. See cdb64.md |
| INDEXES_PUBLISHED_DIR | String | data/indexes/published | Directory the gateway's /ar-io/indexes routes serve from, written by the index-swarm sidecar. Read only; the routes serve only what the signed publication there lists |
| INDEXES_PUBLIC_URL | String | https://<ARNS_ROOT_HOST> | This gateway's public origin, such as https://gateway.example: the base of the absolute URLs in its index feeds (/ar-io/indexes/feed/<index>.xml), and of the WebSeed each served .torrent names. An origin only (scheme, host and optional port; no path), so a gateway served under a path prefix cannot use feeds; a value that is not one stops the gateway at startup. Never taken from a request's Host header. With neither this nor ARNS_ROOT_HOST, the feeds answer 404 and .torrent files are served without a WebSeed |
| INDEXES_TORRENT_WEBSEED | Boolean | true | Whether each served .torrent names this gateway as its WebSeed (<INDEXES_PUBLIC_URL>/ar-io/indexes/webseed/), so a stock BitTorrent client following a feed can finish a band no peer is delivering, while the gateway still offers the band and within its allowance where the byte routes are metered. Such a client pulls a good share of every band from it even while seeders could serve it, through the byte routes' meter when that is on. false serves torrents as the publisher wrote them, for peers only. Subscribing gateways drop it either way |
| INDEXES_ADVERTISE_FROM_URL | String | - | For a node that serves an operator's indexes but does not sign them: base URL of the gateway that does (e.g. http://10.0.0.1:4000). This node then advertises that gateway's publication in /ar-io/info, fetched every minute and advertised only if it names this node's AR_IO_WALLET and its signature verifies. Unset on the signing node |
| INDEX_SWARM_PUBLISH | String (JSON) | unset | JSON array of {name, kind, filter?} describing the indexes this gateway publishes. Each name must match ^[a-z0-9-]{1,64}$, checked at startup. kind is cdb64-root-tx, parquet-l1 or parquet-l2; a parquet-l2 index's filter, if set, must be the selection its bands declare. Read by the index-swarm sidecar only. See index-swarm.md |
| INDEX_SWARM_SUBSCRIBE | String (JSON) | unset | JSON array of {publisher, name?, url?} naming the publishers to subscribe to, by registered gateway wallet. name is an index name or a list of them, each matching ^[a-z0-9-]{1,64}$, checked at startup; without it, every index the publisher offers except opt-in kinds (parquet-l1 and parquet-l2, which must be named). url fetches the publication from that address instead of the publisher's registered one (for a fleet node subscribing to its own publisher internally); the signature is still checked against the registered observer key. List each publisher once; omit name to take all its indexes. Read by the index-swarm sidecar only |
| INDEX_SWARM_TRUSTED_PUBLISHERS | String | unset | Comma-separated gateway wallets. When set, tightens the registry check to this allowlist; it never replaces it |
| INDEX_SWARM_ALLOWED_FILE_ORIGINS | String | unset | Comma-separated http(s)://host[:port] origins, besides a publication's own, that a band may name for its files (a mirror or CDN). A band naming any other server is skipped and counted unreachable: a URL in a signed document is still a request to wherever it points, and from inside the gateway's network that could be an internal service |
| INDEX_SWARM_DATA_DIR | String | data/indexes | Root of the shared volume holding published/, incoming/, installed/ and state.json, as the sidecar sees it. Compose fixes this at /app/data/indexes; to move the volume on the host, set INDEX_SWARM_DATA_PATH instead. Only matters when running the sidecar outside compose |
| INDEX_SWARM_PUBLISH_SCAN_INTERVAL_SECONDS | Number | 60 | How often the publisher looks for new or changed bands. Only bands whose files changed are re-hashed |
| INDEX_SWARM_PUBLISH_TTL_SECONDS | Number | 86400 | How long a publication stays fresh; subscribers alarm once it expires. Set it to about twice the interval at which bands change. See index-swarm.md |
| INDEX_SWARM_SUPERSEDE_GRACE_SECONDS | Number | 300 | How long a retired band's files stay on disk after it stops being served, giving anything mid-read time to finish |
| INDEX_SWARM_POLL_INTERVAL_SECONDS | Number | 300 | How often each publisher is polled for a new document. The reconcile is idempotent and runs every poll, so a band that failed to download earlier is retried without waiting for the publisher to publish again |
| INDEX_SWARM_MANIFEST_FETCH_TIMEOUT_MS | Number | 30000 | Give up on a publisher that has not answered in this long. |
| INDEX_SWARM_DOWNLOAD_STALL_TIMEOUT_SECONDS | Number | 60 | Give up on a band file download once no bytes have arrived for this long; the partial file is kept and resumed on the next poll. Bounds a stall, not the transfer: a large band on a slow link is never cut off while it is still moving. Time spent pacing for INDEX_SWARM_DOWNLOAD_RATE_LIMIT_BYTES_PER_SEC does not count as a stall |
| INDEX_SWARM_DOWNLOAD_CONCURRENCY | Number | 4 | Parallel file downloads within one band |
| INDEX_SWARM_MAX_DISK_BYTES | Number | unset (no ceiling) | Ceiling on the disk index sharing may use; a band that would pass it is skipped (skipped_disk_budget). See index-swarm.md for what counts |
| INDEX_SWARM_DOWNLOAD_RATE_LIMIT_BYTES_PER_SEC | Number | unset (no cap) | Write-rate cap while downloading a band. Set this on a gateway whose index volume is a spinning disk that is also serving reads. The cap covers the whole band: it is shared across the INDEX_SWARM_DOWNLOAD_CONCURRENCY files downloaded at once. |
| INDEX_SWARM_REGISTRY_CACHE_TTL_SECONDS | Number | 300 | How long one read of the gateway's /ar-io/peers, where the sidecar gets registry records, is reused. The gateway refreshes that list hourly; the sidecar makes no RPC calls of its own |
| INDEX_SWARM_METRICS_PORT | Number | 9101 | Port for the sidecar's /metrics and /healthz endpoints |
| INDEX_SWARM_METRICS_HOST | String | 0.0.0.0 | Bind address inside the container. Nothing reaches the host unless the port is mapped |
| INDEX_SWARM_CORE_URL | String | http://core:4000 | Where the sidecar reaches the gateway for the release-compatibility check |
| INDEX_SWARM_MIN_CORE_RELEASE | Number | 84 | Gateway release that first understood a collection source; a pre-release such as 84-pre counts as its release. Below it the subscriber installs nothing until the gateway is upgraded, rechecking each poll |
| INDEX_SWARM_SHUTDOWN_TIMEOUT_MS | Number | 10000 | How long to let work finish on SIGTERM before exiting anyway |
| INDEX_SWARM_LOG_LEVEL | String | info | Sidecar log level |
| INDEX_SWARM_LOG_FORMAT | String | simple | Sidecar log format (simple or json) |
| INDEX_SWARM_LOG_MAX_SIZE | String | 50m | Docker json-file log rotation size for the sidecar, the torrent engine, its init and index-export |
| INDEX_SWARM_LOG_MAX_FILE | Number | 3 | Docker json-file log rotation file count for the sidecar, the torrent engine, its init and index-export |
| INDEX_SWARM_DATA_PATH | String | ./data/indexes | Host path mounted as the sidecar's data directory |
| INDEX_SWARM_OBSERVER_KEYPAIR_FILE | String | unset (compose mounts /dev/null) |
Host path of the observer keypair file (the file the gateway's OBSERVER_KEYPAIR_PATH names, as seen from the host), mounted alone into the sidecar. If the gateway's observer key is its SOLANA_KEYPAIR_PATH file, point this at that file. Set this or OBSERVER_PRIVATE_KEY, not both: with both, publishing refuses to start (Set OBSERVER_KEYPAIR_PATH or OBSERVER_PRIVATE_KEY, not both). The sidecar never sees the rest of the wallets directory |
| INDEX_SWARM_ENGINE_URL | String | http://index-swarm-engine:8080 when INDEX_SWARM_ENGINE_AUTH is set, else unset |
The torrent engine's Web API (qBittorrent-nox 5.2). Set it only for an engine outside the compose profile: with INDEX_SWARM_ENGINE_AUTH set it defaults to the compose engine, and with neither, bands move over HTTP only. An engine that is configured but not answering costs nothing: bands move over HTTP until it answers. The engine runs under the index-swarm-torrent compose profile. Use the host name and port the engine listens on: qBittorrent refuses a Host header that differs, with a 401 on every call. Redirects are refused, so the login never follows one |
| INDEX_SWARM_ENGINE_AUTH | String | - | user:password for the engine's Web API; the password must be at least 16 characters (for example swarm:$(openssl rand -hex 24)), and a shorter or empty one is refused at startup. Required with the compose engine: index-swarm-engine-init refuses to start the engine without it, and the subnet allowlist is kept off. Only an engine run outside the compose profile, configured to let the sidecar in without a login, can leave it unset. Failed logins are not retried: qBittorrent bans an address after five |
| INDEX_SWARM_ENGINE_PORT | Number | 6881 | Peer port the torrent engine listens on, published on the host over TCP and UDP. Open it to inbound peers. Keep it below Linux's ephemeral range (32768–60999): a port inside it can be held by an outbound socket when the engine starts, and the engine then fails to start. If you must use one inside it, reserve it with sysctl net.ipv4.ip_local_reserved_ports=<port> |
| INDEX_SWARM_UPLOAD_LIMIT_BYTES_PER_SEC | Number | 10000000 | Cap on the torrent engine's upload to peers, in bytes a second (10 MB/s by default); 0 is unlimited. Applied by index-swarm-engine-init on every engine start, and restored by the sidecar each day after the daily budget throttled it |
| INDEX_SWARM_UPLOAD_DAILY_LIMIT_BYTES | Number | 100000000000 | Most the torrent engine may upload to peers in a UTC day (100 GB by default); 0 is no budget. Once spent, seeding is throttled to 1 KiB/s until the next day, whatever peers ask for. Downloads and the HTTP tier are unaffected. Counted across engine and sidecar restarts |
| INDEX_SWARM_ENGINE_UID / INDEX_SWARM_ENGINE_GID | Number | 1000 | User and group the torrent engine runs as. swarm/ is handed to them, and the sidecar hands each band's download directory to them before adding a torrent; nothing else is writable to the engine. Set the same values for the sidecar and the engine |
| INDEX_SWARM_ENGINE_CONFIG_PATH | String | ./data/index-swarm-engine | Host directory for the torrent engine's configuration and resume data |
| INDEX_SWARM_ENGINE_CONFIG_DIR | String | /config | Where index-swarm-engine-init writes the engine's configuration, inside its own container. Compose mounts INDEX_SWARM_ENGINE_CONFIG_PATH there; set it only when running the init outside compose |
| INDEX_SWARM_TRACKERS | String | - | Comma-separated tracker announce URLs written into every torrent this node publishes; normally this node's own closed tracker through the gateway, https://<gateway>/ar-io/indexes/announce. Outside the infohash, but inside the .torrent file: publishers that want byte-identical files use the same list. Subscribers hand their engine only trackers on public hosts, and never a WebSeed from the file |
| INDEX_SWARM_ALLOWED_TRACKERS | String | - | Comma-separated tracker announce URLs, exactly as written, that a subscriber hands its torrent engine even though their host is private or a single-label name: an operator's own tracker on a LAN. Every other such tracker is dropped from a publisher's torrent. Only useful with INDEX_SWARM_ENGINE_BLOCK_PRIVATE=false: the engine's IP filter otherwise refuses the tracker's address |
| INDEX_SWARM_TRACKER_PORT | Number | 6969 | Port the closed tracker listens on for direct announces, published on the host. The tracker runs only on a node that publishes torrents, and answers only for the bands it offers. Needs opening only when peers announce to it directly rather than through the gateway |
| INDEX_SWARM_TRACKER_PROXY_PORT | Number | 6970 | Port of the tracker's second listener, which the gateway's Envoy forwards /ar-io/indexes/announce to. Never published. It believes X-Forwarded-For from INDEX_SWARM_TRACKER_PROXY_TRUSTED_PROXIES and INDEX_SWARM_TRACKER_TRUSTED_PROXIES. Set on both the sidecar and Envoy (compose passes it to both). Needs the Envoy image from release 85 or later, which has the route |
| INDEX_SWARM_TRACKER_PROXY_HOST | String | index-swarm | Host Envoy forwards /ar-io/indexes/announce to: the sidecar's service name. Change it only when the sidecar runs elsewhere |
| INDEX_SWARM_TRACKER_TRUSTED_PROXIES | String | - | Comma-separated IPs or CIDRs of proxies in front of the closed tracker whose X-Forwarded-For it believes, on both listeners (on the gateway route, beside INDEX_SWARM_TRACKER_PROXY_TRUSTED_PROXIES). List any proxy that reaches the gateway from a public address, or every peer appears at that proxy's address. The direct port believes only this list |
| INDEX_SWARM_TRACKER_PROXY_TRUSTED_PROXIES | String | every private range | Comma-separated IPs or CIDRs whose X-Forwarded-For hop the tracker's gateway listener (INDEX_SWARM_TRACKER_PROXY_PORT) believes. The default, like core's TRUSTED_PROXIES, needs no setting behind nginx and Envoy, but also believes a client that reaches Envoy from a private address (LAN, tailnet, another container, IPv6 through Docker's userland proxy), which can then list any address as a peer. Where such clients exist, name only the proxies, such as 172.16.0.0/12,<nginx host> |
| INDEX_SWARM_ENGINE_PUBLIC_HOST | String | host of the first INDEX_SWARM_TRACKERS URL | The host (or IP) peers reach this node's torrent engine on, at INDEX_SWARM_ENGINE_PORT. The closed tracker lists this node's own engine under it. Set it when the tracker is served through a load balancer that does not forward the peer port |
| INDEX_SWARM_TORRENT_TIMEOUT_SECONDS | Number | 3600 | How long a subscriber lets a torrent go without progress before abandoning it and fetching the band over HTTP. Measured from the last progress, so a large band that keeps moving is never cut off |
| INDEX_SWARM_WEBSEED_AFTER_SECONDS | Number | 120 | How long a torrent may make no progress before the subscriber turns on the publisher's WebSeed. The WebSeed is the metered tier, so it is added only when peers are not delivering |
| INDEX_SWARM_ENGINE_NETWORK_NAME | String | ar-io-index-swarm-engine | Docker network the torrent engine runs on, shared only with the sidecar, so the engine cannot reach the gateway, the observer or other services |
| INDEX_SWARM_ENGINE_BLOCK_PRIVATE | Boolean | true | Have the torrent engine refuse peers, trackers and WebSeeds on private, loopback, link-local and carrier-grade NAT addresses, so other gateways cannot use it to reach this node's network. Set false only when the swarm itself runs on a private network, as between gateways on one LAN. The filter also refuses a WebSeed on a private address, so a subscription url override pointing at a LAN host needs it false too; the sidecar warns at startup when a setting names a private address |
| INDEX_EXPORT_KINDS | String | root-tx-index | Comma-separated indexes index-export builds: root-tx-index, parquet-l1 (L1 bands from core.db), parquet-l2 (L2 bands of the selections in INDEX_EXPORT_L2_INDEXES, from ClickHouse), any of them. Without root-tx-index, INDEX_EXPORT_SOURCES is not needed; without root-tx-index or parquet-l2, neither is INDEX_EXPORT_HEADER_CHECK_URL. See index-export.md |
| INDEX_EXPORT_CORE_DB | String | data/sqlite/core.db | Path, inside the index-export container (relative to /app), of the core.db it reads L1 bands from, read-only. It must hold the chain from height 0 (no START_HEIGHT). The gateway must be running (a WAL database is read through its -wal and -shm files) |
| INDEX_EXPORT_SOURCES | String (JSON) | this gateway's ClickHouse if CLICKHOUSE_URL is set, else its SQLite |
JSON list of the record sources the index-export service merges: {"type":"clickhouse"} (this gateway's, from CLICKHOUSE_*; CLICKHOUSE_USER must be set), {"type":"clickhouse","url":…,"user":…,"passwordFile":"/run/secrets/index-export/<file>"} (a peer indexer's), {"type":"sqlite"} (bundles.db, read-only) or {"type":"csv","path":…} (an authoritative overlay directory of <from>-<to>.csv files); with "rank":0, a peer's records exported to such files, as where its database isn't reachable). Each may set name and "optional": true (a failure is counted and the run goes on without it). ClickHouse and SQLite are never combined. Read by index-export only. See index-export.md |
| INDEX_EXPORT_RUN_AT_UTC | String | 04:00 | Daily run time, HH:MM UTC. The weekly fold of the recent band runs on the same tick. A day missed (the service was down) runs soon after start |
| INDEX_EXPORT_RECENT_MAX_BLOCKS | Number | 100000 | Span at which the recent band (r) is frozen and a new one starts above it: one more band every this many blocks (about 139 days at the default). Legacy layout only |
| INDEX_EXPORT_ROOT_TX_LAYOUT | String | legacy | How root-TX bands cut the chain: legacy (h, a weekly-folded r, a daily d) or grid (the parquet-l1 grid: 100,000-height h and 5,000-height d bands frozen after a settle lag, and a daily tip; frozen bands re-settled on an interval). Exactly one of the two. See index-export.md |
| INDEX_EXPORT_ROOT_TX_SETTLE_BLOCKS | Number | 5040 | Grid layout: blocks a 5,000-height range waits below the stable top before it is frozen into a d band (5,040 is about 7 days). Whole number, 0 or more |
| INDEX_EXPORT_ROOT_TX_RESETTLE_INTERVAL_HOURS | Number | 168 | Grid layout: hours after a frozen band was built or last re-settled before the daily run re-settles it: rebuilt from its own entries and the sources when the sources' fingerprints changed, and replaced only when entries were added or relocated. 0: never automatically, only with --once --resettle (--resettle=full rebuilds whatever the fingerprints say). At most 876,000 |
| INDEX_EXPORT_ROOT_TX_RESETTLE_MAX_CHANGED | Number | 20000 | Grid layout: the most entries a re-settle may relocate in one band, each header-checked; a band with more is rejected and left as it is |
| INDEX_EXPORT_START_HEIGHT | Number | unset | Lowest height index-export builds. Required for its first run (bootstrap) unless existing bands are adopted; history below it is a separate decision. In the grid layout, a bootstrap starts at the 100,000-height range it falls in, or at 0 when unset |
| INDEX_EXPORT_L1_RUN_BUDGET_MINUTES | Number | 240 (4 hours) | How long an index-export run may keep starting parquet-l1 history bands; the rest wait for the next run, 15 minutes later. The default keeps a daily run off-peak. Raise it for a one-time rebuild or a bootstrap, so one run builds every band. Lookups are derived outside this budget |
| INDEX_EXPORT_L2_INDEXES | String (JSON) | unset (required for parquet-l2) |
JSON list of the L2 indexes to build, each {"name":…,"selection":{"kind":"ans104-data-items","tag":{"name":…,"valuePrefix":…,"excludeValuePrefix":…}}}: an item is in it when one of its tags is named name exactly and its value starts with valuePrefix and not with excludeValuePrefix (optional; it must extend valuePrefix), compared as UTF-8 bytes. Names match [a-z0-9-]{1,64}, are unique, and are not root-tx-index, parquet-l1 or blobs; each index is published under published/<name>/, to be listed in INDEX_SWARM_PUBLISH as {"name":<name>,"kind":"parquet-l2"}. A selection never changes: a new one is a new name. See index-export.md |
| INDEX_EXPORT_L2_SOURCE | String (JSON) | this gateway's ClickHouse | The ClickHouse L2 bands are read from: {"type":"clickhouse"} (this gateway's, from CLICKHOUSE_*; CLICKHOUSE_USER must be set) or {"type":"clickhouse","url":…,"user":…,"passwordFile":"/run/secrets/index-export/<file>"} (a peer's). One source, never optional |
| INDEX_EXPORT_L2_SETTLE_BLOCKS | Number | INDEX_EXPORT_ROOT_TX_SETTLE_BLOCKS (5040) |
Blocks a 5,000-height range waits below the L2 source's stable top before its band is frozen; above, a tip band per sub-range is rebuilt daily. L2 bands re-settle on INDEX_EXPORT_ROOT_TX_RESETTLE_INTERVAL_HOURS, relocating at most INDEX_EXPORT_ROOT_TX_RESETTLE_MAX_CHANGED rows a band |
| INDEX_EXPORT_L2_WALLETS_DB | String | data/sqlite/bundles.db | Path, inside the index-export container, of the bundles.db whose wallets table gives owners' keys ClickHouse doesn't hold, opened read-only; skipped when absent. Keys are kept only when their SHA-256 is the owner's address |
| INDEX_EXPORT_L2_MAX_READS | Number | 200000 | The most reads through INDEX_EXPORT_HEADER_CHECK_URL one L2 band may make, to settle ambiguous offsets by their headers and to read owners' keys from the roots. A band that needs more is rejected (read_budget) |
| INDEX_EXPORT_L2_RUN_BUDGET_MINUTES | Number | 240 (4 hours) | How long an index-export run may keep starting frozen L2 bands (freezes and re-settles); the rest, and the tip with them, wait for the next run, 15 minutes later. Raise it for a bootstrap |
| INDEX_EXPORT_L2_DUCKDB_MEMORY | String | 2GB | DuckDB's memory_limit while an L2 band is built (512MB, 2GB, 1.5GiB). Past it DuckDB spills into the build's staging directory, so keep it, with the 4 GB heap, under the container's mem_limit |
| INDEX_EXPORT_L2_DUCKDB_THREADS | Number | 2 | DuckDB's threads while an L2 band is built, 1 to 64. Fewer threads hold less memory |
| INDEX_EXPORT_L2_DUCKDB_SPILL_MAX_GB | Number | 256 | The most DuckDB may spill into the staging directory while an L2 band is built, in GB; a build past it fails rather than fill the disk |
| INDEX_EXPORT_L2_WINDOW_ITEMS | Number | 50000 | The most items one window of the L2 source holds in memory; a window of more is read again in halves, down to a single height. At least 1,000 |
| INDEX_EXPORT_L2_RECHECK_UNREADABLE_EVERY | Number | 4 | A frozen L2 band built while the gateway answered 404 for some of its reads (ambiguous items left unproven, owners without a key) is rebuilt on every this many re-settles its fingerprint would otherwise skip, so roots served again get in. At least 1 |
| INDEX_EXPORT_HEADER_CHECK_URL | String | unset (required) | Gateway index-export reads sampled item headers from before publishing a band. It must hold the bands' root transactions: http://core:4000 only on a gateway that does (an indexer); otherwise e.g. https://turbo-gateway.com, since a gateway that must fetch roots times out and takes the load. index-swarm-setup --publish sets it |
| INDEX_EXPORT_HEADER_CHECK_TIMEOUT_MS | Number | 30000 | Per range read of the header check |
| INDEX_EXPORT_METRICS_PORT | Number | 9102 | Port of index-export's /metrics and /healthz, inside the container. /healthz reports only that the loop is alive, never how runs went |
| INDEX_EXPORT_SECRETS_DIR | String | unset (compose mounts /dev/null) |
Host directory mounted read-only at /run/secrets/index-export in the index-export container, for peers' ClickHouse password files. Compose only |
| INDEX_EXPORT_IMAGE_TAG | String | CORE_IMAGE_TAG, else latest |
Core image tag the index-export container runs, when it should differ from core's (the service needs a release that includes it). Compose only |
| LOG_LEVEL / LOG_FORMAT (index-export) | String | info / simple | index-export logs by the gateway's LOG_LEVEL and LOG_FORMAT, not the INDEX_SWARM_LOG_* settings |
The index-swarm sidecar also reads settings it shares with the gateway:
OBSERVER_PRIVATE_KEY, or the keypair file given by
INDEX_SWARM_OBSERVER_KEYPAIR_FILE (the key it signs with, read only;
publishing refuses to start without one), and AR_IO_WALLET (the identity it
publishes under, which subscribers resolve in the registry). The gateway's own
OBSERVER_KEYPAIR_PATH is not passed through: it names a path inside the
wallets directory, which the sidecar does not mount. It reads the
registry through the gateway's /ar-io/peers and makes no RPC calls, so it
needs no SOLANA_RPC_URL.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| CDB64_REMOTE_RETRIEVAL_ORDER | String | "chunks" | Comma-separated list of data sources for fetching remote CDB64 files. Options: 'gateways' (trusted gateways), 'chunks' (L1 chunk reconstruction), 'tx-data' (Arweave node /tx/:id/data) |
| CDB64_REMOTE_CACHE_MAX_REGIONS | Number | 100 | Maximum number of byte-range regions to cache per remote CDB64 source |
| CDB64_REMOTE_CACHE_TTL_MS | Number | 300000 | TTL in milliseconds for cached CDB64 byte-range regions (5 minutes) |
| CDB64_REMOTE_REQUEST_TIMEOUT_MS | Number | 30000 | Request timeout in milliseconds for remote CDB64 source requests (30 seconds) |
| CDB64_REMOTE_MAX_CONCURRENT_REQUESTS | Number | 4 | Maximum concurrent HTTP requests across all remote CDB64 sources. Limits request pile-up when reading CDB files from HTTP/S3 endpoints |
| CDB64_REMOTE_SEMAPHORE_TIMEOUT_MS | Number | 5000 | Maximum time in milliseconds to wait for a request slot before failing. Prevents indefinite blocking when concurrent request limit is reached |
| ROOT_TX_CACHE_MAX_SIZE | Number | 10000 | Maximum size of the root transaction ID cache |
| ROOT_TX_CACHE_TTL_MS | Number | 300000 | TTL in milliseconds for root transaction ID cache entries (5 minutes) |
| ROOT_TX_INDEX_CIRCUIT_BREAKER_TIMEOUT_MS | Number | 60000 (CIRCUIT_BREAKER_TIMEOUT_MS) |
Circuit breaker timeout in milliseconds for root transaction index requests |
| ROOT_TX_INDEX_CIRCUIT_BREAKER_FAILURE_THRESHOLD | Number | 5 (CIRCUIT_BREAKER_FAILURE_THRESHOLD) |
Circuit breaker failure threshold percentage for root transaction index requests |
| ROOT_TX_INDEX_CIRCUIT_BREAKER_SUCCESS_THRESHOLD | Number | 2 (CIRCUIT_BREAKER_SUCCESS_THRESHOLD) |
Number of successful requests needed to close circuit breaker for root transaction index |
| WEIGHTED_PEERS_TEMPERATURE_DELTA | Number | 2 | How much a discovered Arweave peer's selection weight (1–100) rises on a success and falls on a failure. Preferred chunk GET/POST peers are not weighted this way (see PREFERRED_CHUNK_GET_NODE_URLS) |
| PEER_MAX_CONCURRENT_OUTBOUND | Number | 10 | Maximum concurrent outbound contiguous data requests per AR.IO peer. Saturated peers are skipped rather than queued. Does not apply to trusted gateway forwarding or chunk retrieval. |
| PEER_CANDIDATE_COUNT | Number | 5 | Number of candidate peers selected for each contiguous data retrieval attempt from AR.IO peers. |
| SKIP_LEAVING_GATEWAYS | Boolean | true | Exclude gateways the AR.IO registry reports as leaving from the peer pool. A gateway leaves either because its operator withdrew it or because the network marked it non-responsive for 30 consecutive epochs, so leaving doubles as a network-consensus liveness signal. Only an explicit leaving is excluded — unknown or absent status is kept, so a registry that stops reporting status degrades to the previous behaviour rather than emptying the peer list |
| PEER_HEDGE_DELAY_MS | Number | 500 | Milliseconds to wait before firing a hedged request to the next candidate peer. Set to 0 for sequential (advance on failure only) behavior. |
| PEER_MAX_HEDGED_REQUESTS | Number | 3 | Maximum number of concurrent hedged requests per getData() call to AR.IO peers. |
| PEER_HASH_RING_VIRTUAL_NODES | Number | 150 | Number of virtual nodes per peer on the consistent hash ring, used for cache-locality peer selection. |
| PEER_HASH_RING_HOME_SET_SIZE | Number | 3 | Number of "home" peers returned by the hash ring for a given data ID. Home peers are prioritized before weighted fallback selection. |
| INSTANCE_ID | String | "" | Adds an "INSTANCE_ID" field to output logs |
| LOG_FORMAT | String | "simple" | Sets the format of output logs, accepts "simple" and "json" |
| SKIP_CACHE | Boolean | false | If true, skips the local cache and always fetches headers from the node |
| SKIP_DATA_CACHE | Boolean | false | If true, skips the data cache (read-through data cache) and always fetches data from upstream sources |
| SKIP_DATA_CACHE_WRITES | Boolean | false | Stop writing new entries to the contiguous data cache while still serving reads from it and keeping the cache index populated. Use this (not SKIP_DATA_CACHE) to stop a full cache volume growing, since SKIP_DATA_CACHE also disables cache reads and index population, which leaves the evictor with nothing to reclaim. Implied by SKIP_DATA_CACHE |
| ENABLE_CONTIGUOUS_DATA_CACHE_TEMP_CLEANUP | Boolean | true | Sweep orphaned staging files from the download staging directories (data/contiguous/tmp, data/tmp/ans-104, data/tmp/data-root). Temp files that outlive their request are always garbage, and no other mechanism reclaims them: the index evictor deletes by content hash and the backfill reconciler only walks the blob tree |
| CONTIGUOUS_DATA_CACHE_TEMP_CLEANUP_THRESHOLD | Number | 21600 (6 hours) | Age in seconds past which a staging file is treated as orphaned. Must exceed the longest legitimate single cache write |
| NEGATIVE_CACHE_ENABLED | Boolean | false | If true, enables the negative data cache that short-circuits 404 responses for repeatedly not-found data IDs |
| NEGATIVE_CACHE_MAX_SIZE | Number | 100000 | Maximum number of entries in the negative cache and miss tracker LRU structures |
| NEGATIVE_CACHE_TTL_MS | Number | 7200000 | Base TTL in milliseconds for negative cache entries (2 hours). Doubles with each re-promotion (exponential backoff) up to NEGATIVE_CACHE_MAX_TTL_MS |
| NEGATIVE_CACHE_MISS_THRESHOLD_MS | Number | 300000 | Duration in milliseconds over which misses must occur before first promotion to negative cache (5 minutes). Accepts 0 to disable duration requirement |
| NEGATIVE_CACHE_MISS_COUNT_THRESHOLD | Number | 10 | Number of misses required before an ID can be promoted to the negative cache. After first promotion, a single miss triggers re-promotion |
| NEGATIVE_CACHE_MISS_TRACKER_TTL_MS | Number | 3600000 | TTL in milliseconds for miss tracker entries (1 hour). Controls how long partial miss counts are retained before expiring |
| NEGATIVE_CACHE_MAX_TTL_MS | Number | 172800000 | Maximum TTL in milliseconds for negative cache entries (48 hours). Caps the exponential backoff growth |
| NEGATIVE_CACHE_PROMOTION_HISTORY_TTL_MS | Number | 604800000 | TTL in milliseconds for promotion history entries (7 days). Controls how long the cache remembers prior promotions for fast re-promotion and backoff |
| NEGATIVE_CACHE_HEALTH_WINDOW_MS | Number | 60000 | Sliding window in milliseconds for tracking success/failure rates (1 minute). Counters reset after each window |
| NEGATIVE_CACHE_UNHEALTHY_THRESHOLD | Number | 0.8 | Failure rate (0-1) above which the system is considered unhealthy and promotions are suppressed. 0.8 means >80% failures suppresses promotions |
| NEGATIVE_CACHE_HEALTH_MIN_SAMPLE_SIZE | Number | 10 | Minimum number of success+failure samples in the health window before the unhealthy threshold is evaluated. Prevents suppression on insufficient data |
| UNTRUSTED_CACHE_RETRY_RATE | Number | 0.1 | Probability (0-1) of stochastically re-verifying cached data from untrusted sources on cache hit |
| TRUSTED_CACHE_RETRY_RATE | Number | 0.0 | Probability (0-1) of stochastically re-verifying cached data from trusted sources on cache hit |
| BACKGROUND_CACHE_RANGE_MAX_SIZE | Number | 10737418240 | Maximum item size (bytes) eligible for a background full-item cache fetch after a range request misses the cache (10 GiB by default). A single-range GET of an uncached item fetches only the range, so this fetch is what puts the whole item in the cache; a concurrent full GET shares it. Signature and owner reads from a parent bundle never trigger it. FOREGROUND_CACHE_MAX_SIZE and FOREGROUND_CACHE_CONCURRENCY apply to it too, and a fetch that receives no data for STREAM_STALL_TIMEOUT_MS is abandoned. 0 disables background caching |
| BACKGROUND_CACHE_RANGE_CONCURRENCY | Number | 4 | Maximum concurrent background full-item cache fetches triggered by range cache misses. A miss beyond the limit is served without starting one |
| FOREGROUND_CACHE_MAX_SIZE | Number | 0 | Maximum item size (bytes) eligible for a foreground (on-demand) full-item cache write. 0 disables the cap. Larger items are still served, just not written to the cache |
| FOREGROUND_CACHE_CONCURRENCY | Number | 0 | Maximum concurrent foreground full-item cache writes. 0 disables the limit. Requests beyond the limit are still served, just not written to the cache. Bounds how much unfinished data can accumulate in contiguous/tmp during a burst of distinct large objects |
| FOREGROUND_CACHE_COALESCE_TIMEOUT_MS | Number | 300000 | How long a request waits to be served by another request's in-flight fetch of the same ID before fetching independently. Bounds the blast radius of a stalled fetch, which would otherwise park every later request for that ID. 0 waits indefinitely |
| FOREGROUND_CACHE_COALESCE_MIN_SIZE | Number | 0 | Smallest known item size (bytes) eligible for foreground coalescing. Below it a request fetches for itself, as it did before coalescing existed. Coalescing costs a waiter the whole download in time-to-first-byte, which is worth paying on a large object and not on a small one. Items of unknown size are eligible, so this can only narrow coalescing where an item is known to be small. 0 disables the floor |
| FOREGROUND_CACHE_COALESCE_MAX_ATTEMPTS | Number | 2 | How many times a request may attach to another's in-flight fetch before fetching for itself. This is leader re-election: with 1, a failed leader releases all its waiters at once and each starts its own fetch in the same tick; with 2, the first waiter back through claims the ID and the rest attach to it. Only a genuine failure re-elects -- a leader that succeeded without caching, or that timed out, sends waiters straight to their own fetch. Each attempt can cost up to FOREGROUND_CACHE_COALESCE_TIMEOUT_MS. Minimum 1 |
| PORT | Number | 4000 | ar.io node exposed port number |
| HTTP_KEEP_ALIVE_TIMEOUT_MS | Number | 60000 | Idle keepalive timeout (ms) for core's HTTP server (server.keepAliveTimeout). Must exceed Envoy's upstream idle_timeout (ENVOY_ARIO_GATEWAY_UPSTREAM_IDLE_TIMEOUT, default 55s) so Envoy recycles pooled connections before core closes them — otherwise Envoy races a request onto a closing connection and clients see an immediate 5xx with no upstream response. |
| HTTP_HEADERS_TIMEOUT_MS | Number | 65000 | Headers timeout (ms) for core's HTTP server (server.headersTimeout). Must be strictly greater than HTTP_KEEP_ALIVE_TIMEOUT_MS (Node requirement). |
| ENVOY_ARIO_GATEWAY_UPSTREAM_IDLE_TIMEOUT | String | "55s" | Envoy upstream idle_timeout for the ario_gateways (core) cluster. Keep below core's HTTP_KEEP_ALIVE_TIMEOUT_MS (default 60s) so Envoy is always the side that recycles idle connections first. Accepts an Envoy duration string (e.g. 55s). |
| ENVOY_ARIO_GATEWAY_MAX_CONNECTIONS | Number | 16384 | Envoy circuit_breakers.max_connections for the ario_gateways (core) cluster. Because that cluster speaks HTTP/1.1, each concurrent request needs its own connection, so this is the effective concurrency ceiling for Envoy→core. Envoy's own default is 1024; at that value a slow core pins the pool, further requests fail immediately with reset reason: overflow (503), and client retries keep it pinned so the cluster never drains. Raise for high-concurrency gateways, lower only alongside the envoy service's nofile ulimit. Watch cluster.ario_gateways.upstream_cx_overflow and the circuit_breakers.*.remaining_cx gauge on the admin /stats endpoint |
| ENVOY_ARIO_GATEWAY_MAX_PENDING_REQUESTS | Number | 16384 | Envoy circuit_breakers.max_pending_requests for the ario_gateways (core) cluster — requests queued waiting for a connection. Exceeding it yields an immediate 503. Envoy's default is 1024. Track via cluster.ario_gateways.upstream_rq_pending_overflow |
| ENVOY_ARIO_GATEWAY_MAX_REQUESTS | Number | 16384 | Envoy circuit_breakers.max_requests for the ario_gateways (core) cluster — total active requests. Binds instead of max_connections if the cluster is ever switched to HTTP/2 multiplexing. Envoy's default is 1024 |
| ENVOY_ARIO_GATEWAY_RETRY_BUDGET_PERCENT | Number | 20 | Envoy circuit_breakers.retry_budget.budget_percent for the ario_gateways (core) cluster: concurrent retries are capped at this percentage of active requests (floor of 16 concurrent). Supersedes Envoy's default max_retries of 3. Keeps the route-level retry policies from amplifying a 5xx storm into self-inflicted load; set lower to make retries less aggressive under stress |
| SIMULATED_REQUEST_FAILURE_RATE | Number | 0 | Number from 0 to 1, representing the probability of a request failing |
| AR_IO_WALLET | String | "" | The gateway's registered wallet: its Solana address, used for staking and rewards, as the publisher of its index bands, and in /ar-io/info |
| ADMIN_API_KEY | String | Generated | API key used for admin API requests (if not set, it's generated and logged into the console) |
| ADMIN_API_KEY_FILE | String | unset | Alternative way to set the API key used for admin API requests via filepath, it takes precedene over ADMIN_API_KEY if defined |
| BACKFILL_BUNDLE_RECORDS | Boolean | false | If true, ar.io node will start indexing missing bundles |
| FILTER_CHANGE_REPROCESS | Boolean | false | If true, all indexed bundles will be reprocessed with the new filters (you can use this when you change the filters) |
| ON_DEMAND_RETRIEVAL_ORDER | String | trusted-gateways,ar-io-network,chunks-offset-aware,tx-data | Data source retrieval order for on-demand data requests. Note: 'chunks-data-item' is deprecated, use 'chunks-offset-aware' instead |
| SKIP_FORWARDING_HEADERS | String | ao-peer-port | Comma-separated list of HTTP headers that indicate a compute-origin request (e.g., from HyperBEAM). When present, remote forwarding to AR.IO peers and trusted gateways is skipped to prevent request loops. Local sources (cache, S3, DB) are still served. |
| SKIP_FORWARDING_USER_AGENTS | String | (empty) | Comma-separated list of User-Agent substrings. Requests whose User-Agent contains any of these substrings (case-insensitive) skip remote forwarding. |
| SKIP_FORWARDING_EMPTY_USER_AGENT | Boolean | true | When true, requests with missing or empty User-Agent headers skip remote forwarding. Catches HTTP clients like Erlang's gun (used by HyperBEAM) that don't send a User-Agent. |
| BACKGROUND_RETRIEVAL_ORDER | String | chunks | Data source retrieval order for background data requests (i.e., unbundling) |
| ENABLE_SAMPLING_DATA_SOURCE | Boolean | false | Enable probabilistic sampling of requests through an experimental data source for A/B testing |
| SAMPLING_DATA_SOURCE | String | undefined | Data source name to sample (e.g., chunks-offset-aware, trusted-gateways). Required when ENABLE_SAMPLING_DATA_SOURCE is true |
| SAMPLING_RATE | Number | 0.1 | Fraction of requests to sample (0.0 to 1.0). 0.1 = 10% of requests |
| SAMPLING_STRATEGY | String | random | Sampling strategy: random (uniform distribution) or deterministic (hash-based, same ID always gets same sampling decision) |
| ANS104_UNBUNDLE_FILTER | String | {"never": true} | Only bundles compliant with this filter will be unbundled |
| ANS104_INDEX_FILTER | String | {"always": true} while ENABLE_BACKGROUND_DATA_VERIFICATION is true (the default), else {"never": true} |
Only bundles compliant with this filter will be indexed |
| ANS104_DOWNLOAD_WORKERS | String | 5 (0 when background verification is off and the unbundle filter is never) | Sets the number of ANS-104 bundles to attempt to download in parallel |
| ANS104_UNBUNDLE_WORKERS | Number | 1 (0 when background verification is off and the unbundle filter is never) | Sets the number of workers used to handle unbundling |
| ANS104_UNBUNDLE_GET_DATA_TIMEOUT_MS | Number | 30000 | Wall-clock timeout (ms) for the Ans104Parser getData fetch before stream processing begins; 0 disables |
| ANS104_UNBUNDLE_STREAM_TOTAL_TIMEOUT_MS | Number | 120000 | Wall-clock timeout (ms) for streaming an ANS-104 bundle to its temp file; complements STREAM_STALL_TIMEOUT_MS; 0 disables |
| ANS104_PARSE_JOB_TIMEOUT_MS | Number | 600000 | Wall-clock timeout (ms) on an in-flight parse job inside an Ans104Parser worker thread. Catches the hang path PR #746 explicitly flagged as "separate follow-up" — worker thread alive but never posts a terminal message, every job it touches stuck indefinitely. On fire: terminate the worker (synthesizes non-zero exit), existing 'exit' handler rejects the job + respawns. Dead bundle goes to bundle-repair-worker. Pair with ans104_parser_job_timeouts_total. Default 10 minutes; tighten if your bundles are smaller. Set to 0 to disable. |
| ANS104_UNBUNDLE_GET_DATA_WALL_CLOCK_TIMEOUT_MS | Number | 300000 | Promise.race wall-clock cap on Ans104Parser.parseBundle's await getData(). Mirrors PR #744's fix for DataImporter.download: the ANS104_UNBUNDLE_GET_DATA_TIMEOUT_MS AbortController.abort() resolves cleanly in ~99% of cases, but ~0.4% of cascade attempts ignore the abort and leave the await hung indefinitely (130+ minute hangs observed in production). When the cap fires, the parseBundle promise rejects and the worker is freed; the underlying cascade promise is abandoned (sockets/peer slots leak until the cascade unwedges — acceptable since the alternative is a permanent worker stall). Default 5 minutes (20× the abort timer) so it fires only for true zombies; "slow but eventually resolves" cases still win on the abort path. Pair with ans104_parser_get_data_wall_clock_fires_total. Set to 0 to disable. |
| DATA_ITEM_FLUSH_COUNT_THRESHOLD | Number | 1000 | Sets the number of new data items indexed before flushing to to stable data items |
| MAX_FLUSH_INTERVAL_SECONDS | Number | 600 | Sets the maximum time interval in seconds before flushing to stable data items |
| WRITE_ANS104_DATA_ITEM_DB_SIGNATURES | Boolean | false | If true, the data item signatures will be written to the database. |
| WRITE_TRANSACTION_DB_SIGNATURES | Boolean | false | If true, the transactions signatures will be written to the database. |
| OPTIMISTIC_TX_INDEXING_ENABLED | Boolean | false | When true, enables POST /ar-io/admin/queue-optimistic-tx (admin-key gated), which lets a trusted poster index a signed L1 transaction optimistically so it is GraphQL-resolvable (with block: null) before it mines. Every tx is signature-verified; the row is inserted with a NULL height and promoted in place when it mines. Default false. While true, the data-verification serving guard ensures such a tx is never served as verified until its root tx is mined. The guard is scoped to this setting: when false, a NULL height means only that this gateway has not imported the tx's block (routine for admin/queue-tx, backfills, or selective sync), so verification proceeds on the data root alone |
| OPTIMISTIC_TX_CLEANUP_WAIT_SECONDS | Number | 7200 | Grace period (seconds) before a never-mined optimistic (NULL-height) transaction is reclaimed from new_transactions by the stale-new-data GC. Must exceed worst-case mine+index latency. Default 2h matches prior hardcoded behavior |
| OPTIMISTIC_TX_MAX_BATCH_SIZE | Number | 100 | Maximum number of transaction headers accepted in a single POST /ar-io/admin/queue-optimistic-tx request. Each accepted tx triggers a sequential signature verification, so this bounds the worst-case work one admin request can schedule (the 10 MB body limit alone would permit thousands of small headers). Requests over the cap are rejected with 400 |
| ENABLE_DATA_DB_WAL_CLEANUP | Boolean | false | If true, the data database WAL cleanup worker will be enabled |
| ENABLE_BACKGROUND_DATA_VERIFICATION | Boolean | true | If true, unverified data will be verified in background |
| MIN_DATA_VERIFICATION_PRIORITY | Number | 60 | Lowest verification priority that background verification picks up. Preferred ArNS names get 80 and other ArNS data 60; data with no priority counts as 0, so a value of 0 or below verifies all unverified data |
| MAX_VERIFICATION_RETRIES | Number | 5 | Background verification stops retrying an item after this many failed attempts |
| ENABLE_DATA_ITEM_ROOT_TX_SEARCH | Boolean | true | If true, enables searching external APIs (GraphQL/Turbo) to find root transaction when local attributes are incomplete for offset-aware data sources |
| ENABLE_PASSTHROUGH_WITHOUT_OFFSETS | Boolean | true | If true, allows data retrieval without offset information in offset-aware data sources (falls back to less efficient methods) |
| MAX_DATA_ITEM_QUEUE_SIZE | Number | 100000 | Sets the maximum number of data items to queue for indexing before skipping indexing new data items |
| DATA_ITEM_INDEXER_QUEUE_SIZE | Number | 500000 | Hard cap on DataItemIndexer's queue. Non-prioritized items pushed at the cap are dropped (counted in data_items_dropped_total{queue_name="dataItemIndexer"}); recovery is via bundle-repair-worker. Set to 0 to disable the cap. |
| DATA_ITEM_INDEXER_WORKER_COUNT | Number | 1 | Fastq concurrency for DataItemIndexer.indexDataItem. Default 1 awaits each saveDataItem serially. Raising lets the main thread pipeline JS prep (event emit, metric inc, log) for the next item while the prior saveDataItem is in flight to the SQLite worker thread. Upper-bound on speedup is gated by the SQLite worker (single-threaded per database). Useful when the failed-bundle backlog (large bundles, 50-500 items each) is the dominant load. |
| QUEUE_DATA_ITEM_MAX_BATCH_SIZE | Number | 5000 | Maximum number of data item headers accepted per POST /ar-io/admin/queue-data-item request. Larger batches are rejected with 400 (instead of a crude body-size 413) so the client splits the request. The body limit is also raised to 10 MB to admit legitimate bundle-sized batches. |
| QUEUE_DATA_ITEM_BACKPRESSURE_DEPTH | Number | 100000 | POST /ar-io/admin/queue-data-item returns 503 + Retry-After when the DataItemIndexer queue depth exceeds this. Admin POSTs are prioritized (they bypass the drop cap via unshift), so this sheds load back to the client under sustained overload instead of ballooning the queue / starving the regular unbundling pipeline. Measured basis: the single bundles-writer saveDataItem is p50 ~2 ms but fat-tailed (p99 ~2 s); 100k sits ~10× above legitimate burst peaks and bounds the prioritized queue to ~150 MB. |
| ANS104_DATA_INDEXER_QUEUE_SIZE | Number | 500000 | Hard cap on Ans104DataIndexer's queue. Items pushed at the cap are dropped (counted in data_items_dropped_total{queue_name="ans104DataIndexer"}); recovery is via bundle-repair-worker. Set to 0 to disable the cap. |
| GRAPHQL_RESOLVER_DEADLINE_MS | Number | 12000 | Server-side hard deadline (ms) for GraphQL resolvers. Composed with the express request signal — when the client disconnects OR this deadline elapses, downstream attribute fetches and arweaveClient requests are cancelled and zombie work doesn't accumulate on trustedNodeRequestQueue. Set to 0 to disable the deadline (only the client-disconnect signal will fire). |
| BUNDLE_DATA_ITEM_DRAIN_BATCH | Number | 100 | Number of ANS104_DATA_ITEM_MATCHED items dispatched into the data-item indexers per setImmediate drain cycle. Buffering and batching the matched-item firehose from the unbundler worker thread prevents JS-thread starvation when large bundles produce thousands of cross-thread messages per second. Larger values reduce per-batch overhead at the cost of fewer event-loop turns; smaller values yield to I/O more often. Buffer depth is exposed as queue_length{queue_name="matchedItemBuffer"}. Must be a positive integer. |
| ARNS_ROOT_HOST | String | undefined | Domain name(s) for ArNS host. Supports comma-separated values for multiple hosts (e.g., arweave.dev,g8way.io). The first host is the "primary" used for gateway identity. ArNS subdomains, apex content, and sandbox redirects work on all configured hosts. |
| SANDBOX_PROTOCOL | String | undefined | Protocol (http/https) for ArNS sandbox redirects and x402 payment resource URLs. Set to 'https' when behind a reverse proxy/CDN. Used when ARNS_ROOT_HOST is set. |
| START_WRITERS | Boolean | true | If true, start indexing blocks, tx, ANS104 bundles |
| RUN_OBSERVER | Boolean | true | If true, runs the Observer alongside the gateway to generate Network compliance reports |
| MIN_RELEASE_NUMBER | String | 0 | Sets the minimum Gateway release version to check while doing a gateway version assessment |
| AR_IO_NODE_RELEASE | String | the release in docker-compose.yaml, else the image's own |
Sets the current ar.io node version to be set on X-AR-IO-Node-Release header on requests to the reference gateway |
| OBSERVER_WALLET | String | "" | The public wallet address of the wallet being used to sign report upload transactions and contract interactions for Observer |
| CHUNKS_DATA_PATH | String | "./data/chunks" | Sets the location for chunked data to be saved. If omitted, chunked data will be stored in the data directory |
| CONTIGUOUS_DATA_PATH | String | "./data/contiguous" | Sets the location for contiguous data to be saved. If omitted, contiguous data will be stored in the data directory |
| HEADERS_DATA_PATH | String | "./data/headers" | Sets the location for header data to be saved. If omitted, header data will be stored in the data directory |
| SQLITE_DATA_PATH | String | "./data/sqlite" | Sets the location for sqlite indexed data to be saved. If omitted, sqlite data will be stored in the data directory |
| CORE_SQLITE_READ_WORKER_COUNT | Number | 1 | Number of reader worker threads for the core SQLite pool. Reads are serialized within a single worker thread, so raising this adds read concurrency for the core DB (WAL supports concurrent readers). Writers stay single (SQLite allows one writer). Watch standalone_sqlite_method_queue_wait_seconds vs ..._service_seconds to judge whether raising it helps. |
| DATA_SQLITE_READ_WORKER_COUNT | Number | 2 | Number of reader worker threads for the data SQLite pool. |
| SQLITE_SLOW_QUERY_LOG_THRESHOLD_MS | Number | 1000 | Operations whose total time (queue wait + service) meets or exceeds this threshold are logged at warn with a queue/service breakdown and a bounded argument summary, to identify which method is responsible during a jam. |
| DUCKDB_DATA_PATH | String | "./data/duckdb" | Sets the location for duckdb data to be saved. If omitted, duckdb data will be stored in the data directory |
| TEMP_DATA_PATH | String | "./data/tmp" | Sets the location for temporary data to be saved. If omitted, temporary data will be stored in the data directory |
| OBSERVER_STATE_PATH | String | "./data/observer" | Sets the location for Observer state data to be saved. Used to persist continuous observation state across restarts |
| LMDB_DATA_PATH | String | "./data/lmdb" | Sets the location for LMDB data to be saved. If omitted, LMDB data will be stored in the data directory |
| SECRETS_PATH | String | "./secrets" | Sets the location for sensitive configuration files (e.g., CDP API keys). Mounted read-only in container for security |
| CHAIN_CACHE_TYPE | String | "redis" | Sets the method for caching chain data, defaults redis if gateway is started with docker-compose, otherwise defaults to LMDB |
| REDIS_CACHE_URL | String (URL) | "redis://redis:6379" (compose); "redis://localhost:6379" outside Docker | URL of Redis database to be used for caching |
| REDIS_CACHE_TTL_SECONDS | Number | 28800 | TTL value for Redis cache, defaults to 8 hours (28800 seconds) |
| ENABLE_FS_HEADER_CACHE_CLEANUP | Boolean | true if starting with docker, otherwise false | If true, periodically deletes cached header data |
| ENABLE_CHUNK_SYMLINK_CLEANUP | Boolean | true | If true, periodically removes dead symlinks from chunk cache directories (symlinks pointing to expired cached data) |
| CHUNK_SYMLINK_CLEANUP_INTERVAL | Number | 86400 | Interval in seconds between dead symlink cleanup runs (default: 24 hours) |
| NODE_MAX_OLD_SPACE_SIZE | Number | 2048 (8192 when ANS104_UNBUNDLE_WORKERS is above 1) |
Sets the memory limit, in megabytes, for Node.js in the core container (docker-entrypoint.sh). Default 2048, or 8192 with more than one unbundle worker |
| SUBMIT_CONTRACT_INTERACTIONS | Boolean | true | If true, Observer will submit its generated reports to the ar.io Network by signing the save_observations ix with OBSERVER_KEYPAIR_PATH ?? SOLANA_KEYPAIR_PATH. Pre-flight-skips when the observer isn't prescribed, already submitted, or past the observation window (no wasted tx fees on bouncing simulations). |
| CRANK_POLL_INTERVAL_MS | Number | derived from epoch duration | Observer epoch-cranker poll cadence, in milliseconds. Only applies when ENABLE_EPOCH_CRANKING=true. Leave unset to let the observer derive the cadence from the epoch duration; an explicit value always wins. Must be a positive integer when set. |
| CLEANUP_MIN_INTERVAL_MS | Number | derived from epoch duration | Minimum interval, in milliseconds, between observer cleanup passes. Only applies when ENABLE_CLEANUP=true. Leave unset to let the observer derive the cadence from the epoch duration; an explicit value always wins. Must be a positive integer when set. |
| CLEANUP_TO_RETURNED_TXS_PER_CYCLE | Number | 10 | Maximum prune_name_to_returned transactions the cranker submits per cleanup scan. Only applies when ENABLE_CLEANUP=true. The instruction takes a single ArNS record, so unlike the other prune steps it cannot batch into one transaction and this sets its throughput. One per scan caps conversion at roughly the rate leases expire, so an expired-lease backlog never drains and each name that ages out of its return-auction window loses that auction permanently. Requires observer image 15e285b0 or newer. |
| CRANK_BATCH_SIZE | Number | 15 | Gateway/observer accounts processed per tally and distribute transaction. Only applies when ENABLE_EPOCH_CRANKING=true. |
| CRANK_CLOSE_EPOCHS | Boolean | true | Close fully-distributed epochs past CRANK_EPOCH_RETENTION, refunding their PDA rent to the cranker wallet. Closing an epoch also closes its Observation PDAs, which removes that epoch's per-observer report detail from chain — set false to keep the detail readable at the cost of the rent. |
| CRANK_EPOCH_RETENTION | Number | 7 | Number of recent epochs to keep before closing. The cranker closes epoch current - CRANK_EPOCH_RETENTION, so this is how many epochs of on-chain history remain queryable. Note close_observation and close_epoch are permissionless: another operator's cranker running a lower retention will close epochs regardless of this setting. |
| CRANK_WARN_BALANCE_SOL | Number | 0.3 | Log a warning when the cranker wallet's SOL balance falls below this. |
| CRANK_CRITICAL_BALANCE_SOL | Number | 0.1 | Log a critical alert when the cranker wallet's SOL balance falls below this. Cranking stops once the wallet cannot pay transaction fees. |
| CLEANUP_BATCH_SIZE | Number | 15 | Accounts pruned per cleanup transaction (prune_expired_names, prune_returned_names). Only applies when ENABLE_CLEANUP=true. |
| CLEANUP_FAILURE_THRESHOLD | Number | 30 | Consecutive-failure count at which a gateway becomes eligible for prune_gateway. Mirrors the on-chain EpochSettings.max_consecutive_failures. |
| MAX_CLEANUP_TXS_PER_CYCLE | Number | 50 | Hard cap on cleanup transactions submitted per cleanup cycle, shared across all cleanup phases. Bounds gas spend if discovery returns thousands of stale accounts. |
| ALT_RECLAIM_SCAN_LIMIT | Number | 200 | Recent signatures scanned per pass when reclaiming rent from single-use prescribe Address Lookup Tables (~0.0126 SOL each). Set 0 to disable reclamation. Reclaiming a prescribe ALT removes the observer index for its epoch, so getEpoch can no longer enumerate that epoch's reports even while the underlying accounts live. |
| OBSERVED_GATEWAY_HOSTS | String | (all registered gateways) | Comma-separated list restricting which gateway hosts this observer assesses. Empty observes every gateway in the registry. Do not put spaces after the commas — entries are split on , and are not trimmed, so a.com, b.com yields a second entry of " b.com" that matches no gateway. Use a.com,b.com. |
| REDIS_MAX_MEMORY | String | 256mb | Sets the max memory allocated to Redis |
| REDIS_DATA_PATH | String | "./data/redis" | Sets the location for Redis data persistence files (dump.rdb, appendonly.aof). Only used if persistence is enabled via EXTRA_REDIS_FLAGS |
| EXTRA_REDIS_FLAGS | String | --save "" --appendonly no | Additional CLI flags passed to Redis server. Default disables persistence for performance. Set to "--save 300 10 --appendonly yes --appendfsync everysec" to enable hybrid persistence (recommended for x402 paid tokens) |
| ARWEAVE_PEER_CHUNK_GET_MAX_PEER_ATTEMPT_COUNT | Number | 8 | Maximum number of Arweave peers to try sequentially when fetching a chunk via GET before giving up. Each attempt is capped at 500 ms, so a miss costs at most attempts × 500 ms |
| ARWEAVE_PEER_CHUNK_GET_PEER_SELECTION_COUNT | Number | 10 | Number of candidate peers to select from each pool (bucket and general) for chunk GET requests |
| PREFERRED_CHUNK_GET_NODE_URLS | String | data-1…17 and tip-1…4.arweave.xyz (:1984) | Comma-separated Arweave node URLs tried first for chunk GETs, before nodes that advertise the chunk's range (/sync_buckets) and other peers. They are ordered by a cost each gateway learns: an average of time-to-success, where any miss (404, error, or the 500 ms per-peer timeout) costs the full timeout. So a gateway uses the preferred nodes that hold chunks and answer fast from where it runs; nodes that are far away or miss sink. Override to pin specific nodes |
| PREFERRED_CHUNK_POST_NODE_URLS | String | tip-1…4.arweave.xyz (:1984) | Comma-separated Arweave node URLs prioritized for chunk broadcast (POST /chunk) |
| ARWEAVE_CHUNK_GET_GEOMETRY_TIMEOUT_MS | Number | 5000 | Per-request timeout (ms) for TX geometry resolution (getTxOffset/getTxField) used during chunk retrieval |
| ARWEAVE_CHUNK_GET_GEOMETRY_RETRY_COUNT | Number | 2 | Number of retries for TX geometry resolution requests during chunk retrieval |
| LEGACY_AWS_S3_CHUNK_DATA_BUCKET | String | undefined | S3 bucket name for legacy chunk data source. Required when 'legacy-s3' is in CHUNK_DATA_RETRIEVAL_ORDER |
| LEGACY_AWS_S3_CHUNK_DATA_PREFIX | String | undefined | Optional prefix for chunk data in the legacy S3 bucket. If omitted, the root of the bucket will be /{dataRoot}/{relativeOffset} |
| LEGACY_AWS_S3_ACCESS_KEY_ID | String | undefined | AWS access key for legacy S3 chunk bucket (optional - falls back to AWS_ACCESS_KEY_ID if not set) |
| LEGACY_AWS_S3_SECRET_ACCESS_KEY | String | undefined | AWS secret key for legacy S3 chunk bucket (optional - falls back to AWS_SECRET_ACCESS_KEY if not set) |
| LEGACY_AWS_S3_REGION | String | undefined | AWS region for legacy S3 chunk bucket. Required if using separate credentials (LEGACY_AWS_S3_ACCESS_KEY_ID) |
| LEGACY_AWS_S3_ENDPOINT | String | undefined | Custom endpoint for legacy S3 chunk bucket (optional - for S3-compatible services) |
| LEGACY_PSQL_CONNECTION_STRING | String | undefined | PostgreSQL connection URL for legacy chunk metadata source (format: postgresql://user:pass@host:port/database). Required when 'legacy-psql' is in CHUNK_METADATA_RETRIEVAL_ORDER |
| LEGACY_PSQL_PASSWORD_FILE | String | undefined | File path containing PostgreSQL password (alternative to including password in connection string for better security) |
| LEGACY_PSQL_SSL_REJECT_UNAUTHORIZED | Boolean | true | If false, allows connections to PostgreSQL servers with self-signed certificates (common workaround for cloud providers) |
| LEGACY_PSQL_MAX_CONNECTIONS | Number | 10 | Maximum number of connections in the PostgreSQL connection pool |
| LEGACY_PSQL_IDLE_TIMEOUT_SECONDS | Number | 30 | Time in seconds before idle connections are closed in the pool |
| LEGACY_PSQL_CONNECT_TIMEOUT_SECONDS | Number | 10 | Maximum time in seconds to wait when establishing a new connection |
| LEGACY_PSQL_MAX_LIFETIME_SECONDS | Number | 1800 | Maximum lifetime in seconds for a connection before it's rotated (30 minutes default) |
| LEGACY_PSQL_STATEMENT_TIMEOUT_MS | Number | 5000 | Server-side query timeout in milliseconds. Prevents queries from running forever. Critical for preventing system hangs |
| LEGACY_PSQL_IDLE_IN_TRANSACTION_TIMEOUT_MS | Number | 10000 | Server-side timeout in milliseconds for idle transactions. Cleans up stuck transactions that hold locks |
| WEBHOOK_TARGET_SERVERS | String | undefined | Target servers for webhooks. Logs redact query values and userinfo but keep the path, so put any credential in the query (e.g. ?token=) or userinfo, not the path |
| WEBHOOK_INDEX_FILTER | String | {"never": true} | Only emit webhooks for transactions and data items compliant with this filter |
| WEBHOOK_BLOCK_FILTER | String | {"never": true} | Only emit webhooks for blocks compliant with this filter |
| CONTIGUOUS_DATA_CACHE_CLEANUP_THRESHOLD | Number | undefined | Sets the age threshold in seconds; files older than this are candidates for contiguous data cache cleanup |
| CONTIGUOUS_DATA_CACHE_CLEANUP_INITIAL_DELAY | Number | CONTIGUOUS_DATA_CACHE_CLEANUP_THRESHOLD |
Delay in seconds before the first contiguous data cache cleanup runs (warm-up for the metadata cache). Defaults to the cleanup threshold when unset. Not persisted, so it restarts on every process restart |
| CONTIGUOUS_DATA_CACHE_LOW_WATERMARK_PERCENT | Number | 0 (disabled) | Low disk-usage watermark (percent of the cache filesystem). Interpreted differently by the two reclaimers: with the filesystem-walk worker (ENABLE_CONTIGUOUS_DATA_CACHE_INDEX unset/false) it's a skip threshold — below it, cleanup is skipped entirely and the cache grows; with the index evictor (ENABLE_CONTIGUOUS_DATA_CACHE_INDEX=true) it's the drain-down-to target — once eviction triggers, it runs until usage falls below this. 0 disables. See docs/cache-cleanup.md |
| CONTIGUOUS_DATA_CACHE_HIGH_WATERMARK_PERCENT | Number | 0 (disabled) | High disk-usage watermark (percent of the cache filesystem). With the filesystem-walk worker (ENABLE_CONTIGUOUS_DATA_CACHE_INDEX unset/false), at/above it cleanup escalates: the effective TTL is progressively tightened (from the normal threshold here toward the min-age floor at a full disk) until usage recovers below the low watermark. With the index evictor (ENABLE_CONTIGUOUS_DATA_CACHE_INDEX=true), at/above it eviction is triggered (least-recently-accessed first, general tier before preferred) and drains to the low watermark. 0 disables. See docs/cache-cleanup.md |
| CONTIGUOUS_DATA_CACHE_MIN_FREE_BYTES | Number | 0 (disabled) | Hard free-space guardrail (both reclaimers): if free bytes on the cache filesystem fall below this, force reclamation regardless of the watermark percentages — maximum-pressure cleanup for the filesystem-walk worker, or immediate eviction for the index evictor. Protects the shared volume (SQLite DBs/WAL/logs) from ENOSPC. 0 disables the guardrail |
| CONTIGUOUS_DATA_CACHE_AGGRESSIVE_MIN_AGE_SECONDS | Number | 3600 | Absolute floor for aggressive cleanup: data younger than this is never evicted even under maximum disk pressure. Filesystem-walk worker only — the index evictor (ENABLE_CONTIGUOUS_DATA_CACHE_INDEX=true) evicts strictly by least-recently-accessed and ignores this |
| ENABLE_CHUNK_DATA_CACHE_CLEANUP | Boolean | true | Run the filesystem cleanup walk over the chunk data cache. Disabling it stops all chunk cache reclamation, so the cache grows unbounded; prefer the watermarks below, which idle the walk when the disk is healthy and wake it under pressure. |
| CHUNK_DATA_CACHE_CLEANUP_THRESHOLD | Number | 14400 (4 hours) | Age in seconds after which cached chunk data is eligible for deletion. 0 disables age-based deletion. |
| CHUNK_DATA_CACHE_LOW_WATERMARK_PERCENT | Number | 0 (disabled) | Integer 0..100; must be below CHUNK_DATA_CACHE_HIGH_WATERMARK_PERCENT when both are enabled. Startup fails otherwise. Skip the chunk cache cleanup walk entirely while cache-filesystem usage is below this percent; 0 always walks. The walk is metadata-bound, so on a cache with tens of millions of inodes atop spinning storage a full pass cannot complete within FS_CLEANUP_WORKER_RESTART_PAUSE_DURATION — it then runs continuously and saturates the device even with ample free space. Set this to make cleanup respond to real disk pressure instead. |
| CHUNK_DATA_CACHE_HIGH_WATERMARK_PERCENT | Number | 0 (disabled) | Integer 0..100. Escalate to aggressive chunk cache cleanup at/above this percent, draining back down to CHUNK_DATA_CACHE_LOW_WATERMARK_PERCENT. 0 disables escalation. The worker latches into the aggressive regime here and clears it only below the low watermark, so the pair defines a sawtooth rather than a single trip point. |
| CHUNK_DATA_CACHE_MIN_FREE_BYTES | Number | 0 (disabled) | Non-negative integer bytes (not 50GiB-style suffixes). Force aggressive chunk cache cleanup when free space drops below this, regardless of the percent watermarks. 0 disables. Breaching it sets pressure to 1 in a single step rather than ramping, which quadruples batch size and floors the inter-batch pause at 250 ms; on an already-full volume that is an I/O cliff, so prefer the percent watermarks. |
| CHUNK_DATA_CACHE_AGGRESSIVE_MIN_AGE_SECONDS | Number | 3600 (1 hour) | Hard floor under aggressive chunk cache cleanup: never evict chunk data younger than this, however tight disk pressure gets. Applied as max(CHUNK_DATA_CACHE_CLEANUP_THRESHOLD * (1 - pressure), this), so whichever is larger wins. Set it above the scaled threshold and it becomes the effective retention — escalation then stops tightening retention and only raises batch size and lowers the inter-batch pause. Size it against how long freshly ingested chunks must remain readable, not against disk headroom. |
| FS_CLEANUP_WORKER_WALK_CONCURRENCY | Number | max(1, floor(UV_THREADPOOL_SIZE / 16)) | Max concurrent readdir/stat syscalls during the filesystem cleanup tree walk. Raising it hides per-directory I/O latency on large, deeply-sharded caches (parallel traversal); lower it if the walk contends with request I/O. Caution: every walk stat() is served by libuv's thread pool (UV_THREADPOOL_SIZE, default 4), shared with the request path. Workers each hold their own limiter, so the total across all enabled cleanup workers is what competes with request handling. Oversubscribing it starves HTTP -- the event loop and CPU stay healthy while the process cannot answer, including its own health check. Keep the total below half the pool; a startup warning fires otherwise |
| ENABLE_CONTIGUOUS_DATA_CACHE_INDEX | Boolean | false | Enables the SQLite cache cleanup index: each cache write records {hash, size, cached_at, tier} and a disk-pressure evictor reclaims by querying the (SSD-backed) index instead of walking the HDD-backed cache tree. Uses the LOW/HIGH_WATERMARK_PERCENT + MIN_FREE_BYTES thresholds above |
| CONTIGUOUS_DATA_CACHE_INDEX_EVICTION_INTERVAL_MS | Number | 60000 | How often (ms) the index-driven evictor checks disk pressure |
| CONTIGUOUS_DATA_CACHE_INDEX_EVICTION_BATCH_SIZE | Number | 1000 | Max index rows evicted (and blobs unlinked) per batch within an eviction sweep |
| CONTIGUOUS_DATA_CACHE_INDEX_UNLINK_CONCURRENCY | Number | max(1, floor(UV_THREADPOOL_SIZE / 8)) | Concurrent blob unlinks per eviction batch. Each unlink holds a libuv thread, so this is derived from the pool rather than fixed: a large value takes the whole pool and queues every other file operation behind it, on a disk that is already saturated whenever the evictor runs |
| CONTIGUOUS_DATA_CACHE_INDEX_MAX_BATCHES_PER_SWEEP | Number | 50 | Batches per sweep. EVICTION_BATCH_SIZE * this bounds the unlinks a single sweep can issue, so a large backlog is reclaimed over several sweeps instead of one pass holding the disk |
| CONTIGUOUS_DATA_CACHE_INDEX_UPDATE_ON_READ | Boolean | true | Refresh a cache entry's last_access (and promote its tier on a preferred-ArNS read) on cache hits => LRU eviction. Set false for FIFO by cache-write time (e.g. operators without an edge cache who want to avoid the per-hit index writes) |
| ENABLE_CONTIGUOUS_DATA_CACHE_INDEX_BACKFILL | Boolean | false | One-time backfill: on startup, walk the existing on-disk cache once and seed index rows for untracked blobs (insert-if-absent, so live entries are untouched). Needed to adopt a pre-existing cache that predates the index; enable once, then disable. Runs in the background without blocking startup. Resumable across restarts (checkpoints per top-level shard); delete data/contiguous/.cache-index-backfill-checkpoint to force a fresh full pass |
| CONTIGUOUS_DATA_CACHE_INDEX_BACKFILL_BATCH_SIZE | Number | 2000 | Rows buffered per backfill insert transaction |
| ENABLE_CHUNK_DATA_CACHE_INDEX | Boolean | false | Enables the SQLite chunk data cache eviction index (ADR 005): each chunk write records {data_root, size, last_write, last_access, tier} and a disk-pressure evictor reclaims whole data roots by querying the index instead of walking the deeply-sharded chunk tree. Uses the CHUNK_DATA_CACHE_* watermarks above. The eviction age floor is derived, not an env var: max(CHUNK_DATA_CACHE_AGGRESSIVE_MIN_AGE_SECONDS, CHUNK_INGEST_CONFIRMATION_TIMEOUT_SECONDS, CHUNK_INGEST_ALLOWLIST_CONFIRMATION_TIMEOUT_SECONDS) when CHUNK_INGEST_CACHE_ENABLED=true, else just CHUNK_DATA_CACHE_AGGRESSIVE_MIN_AGE_SECONDS. It is derived so it cannot drift below the ingest confirmation window, which would silently break upload propagation |
| CHUNK_DATA_CACHE_INDEX_EVICTION_INTERVAL_MS | Number | 60000 | How often (ms) the index-driven chunk evictor checks disk pressure |
| CHUNK_DATA_CACHE_INDEX_EVICTION_BATCH_SIZE | Number | 1000 | Max index rows considered (and chunks unlinked) per batch within an eviction sweep |
| CHUNK_DATA_CACHE_INDEX_UNLINK_CONCURRENCY | Number | max(1, floor(UV_THREADPOOL_SIZE / 8)) | Concurrent data-root removals per eviction batch. Each fs.rm holds a libuv thread for its whole walk, so this is derived from the pool rather than fixed |
| CHUNK_DATA_CACHE_INDEX_EVICTION_TARGET_BYTES | Number | 1073741824 (1 GiB) | Bytes the evictor aims to free per eviction batch. Chunk eviction is size-aware rather than purely coldest-first: the median data root holds only ~2 chunks, so a coldest-first batch of N rows can free almost nothing while still paying N index reads and N unlinks |
| CHUNK_DATA_CACHE_INDEX_UPDATE_ON_READ | Boolean | true | Refresh a data root's last_access on chunk cache hits => LRU eviction. Set false for FIFO by cache-write time (e.g. operators fronted by an edge cache, where the core sees too few reads for recency to mean much). Never advances last_write, so it cannot move the age floor |
| ENABLE_CHUNK_DATA_CACHE_INDEX_BACKFILL | Boolean | false | One-time backfill: on startup, walk the existing on-disk chunk cache once and seed index rows for untracked data roots (insert-if-absent, so live entries are untouched). Needed to adopt a pre-existing cache that predates the index; enable once, then disable. Runs in the background without blocking startup |
| CHUNK_DATA_CACHE_INDEX_BACKFILL_BATCH_SIZE | Number | 2000 | Rows buffered per backfill insert transaction |
| ENABLE_CHUNK_DATA_CACHE_INDEX_HYBRID_TAIL | Boolean | false | Reserved — currently inert. Placeholder for ADR 005 Departure 2 (leave small "tail" data roots to the filesystem-walk worker and index only large ones). Nothing reads it yet; enabling it does nothing |
| CHUNK_DATA_CACHE_INDEX_HYBRID_TAIL_CHUNK_THRESHOLD | Number | 100 | Reserved — currently inert. Chunk count at or below which a data root would be treated as "tail" once the hybrid-tail behavior is implemented |
| UV_THREADPOOL_SIZE | Number | 4 (Node.js default) | Node.js libuv threadpool size for filesystem/DNS/crypto operations. Raising it (e.g. to 16) reduces contention for the filesystem cache-cleanup walk under heavy load. Process-wide; affects all workers |
| ENABLE_MEMPOOL_WATCHER | Boolean | false | If true, the observer will start indexing pending tx from the mempool |
| MEMPOOL_POLLING_INTERVAL_MS | Number | 30000 | Sets the mempool polling interval in milliseconds |
| TAG_SELECTIVITY | String | a built-in map (TAG_SELECTIVITY in src/config.ts) |
A JSON map of tag names to selectivity weights used to order SQLite tag joins |
| GET_DEBUG_INFO_CACHE_TTL_MS | Number | 300000 | Cache TTL (ms) for the /ar-io/admin/debug SQLite snapshot. Each call runs unfiltered COUNT(*) scans on the SQLite worker, so caching avoids monopolizing the debug worker when the endpoint is polled frequently. Set to 0 to disable caching. |
| MAX_EXPECTED_DATA_ITEM_INDEXING_INTERVAL_SECONDS | Number | undefined | Sets the max expected data item indexing interval in seconds |
| AR_IO_SQLITE_BACKUP_S3_BUCKET_NAME | String | "" | S3-compatible bucket name, used by the Litestream backup service |
| AR_IO_SQLITE_BACKUP_S3_BUCKET_REGION | String | "" | S3-compatible bucket region, used by the Litestream backup service |
| AR_IO_SQLITE_BACKUP_S3_BUCKET_ACCESS_KEY | String | "" | S3-compatible bucket access_key credential, used by Litestream backup service, omit if using resource-based IAM role |
| AR_IO_SQLITE_BACKUP_S3_BUCKET_SECRET_KEY | String | "" | S3-compatible bucket access_secret_key credential, used by Litestream backup service, omit if using resource-based IAM role |
| AR_IO_SQLITE_BACKUP_S3_BUCKET_PREFIX | String | "" | A prepended prefix for the S3 bucket where SQLite backups are stored. |
| ARNS_MAX_CONCURRENT_RESOLUTIONS | Number | 1 | Maximum number of concurrent resolutions allowed for ARNs. |
| ARNS_NAME_LIST_PAGE_SIZE | Number | 10000 | Page size used when walking the ArNS registry during name-cache hydration. The Solana-backed getArNSRecords has no server-side cursor: every call runs a full getProgramAccounts scan of the ArNS program and slices client-side, so a page smaller than the registry repeats that scan once per page. Backends that cap the page server-side simply return fewer items plus a cursor. Must be a positive integer. |
| ARNS_NAME_LIST_MAX_RETRIES | Number | 3 | Per-page fetch attempts made by the name-cache hydration loop, on top of any retries the reader performs internally (the Solana reader wraps each call in the SDK's 3-attempt withRetry, so the worst case is this value times three full registry scans for one page). Must be a positive integer. |
| AWS_ACCESS_KEY_ID | String | undefined | AWS access key ID for accessing AWS services |
| AWS_SECRET_ACCESS_KEY | String | undefined | AWS secret access key for accessing AWS services |
| AWS_REGION | String | undefined | AWS region where the resources are located |
| AWS_ENDPOINT | String | undefined | Custom endpoint for AWS services |
| TURBO_AWS_REGION | String | undefined | AWS region for the Turbo S3 data source. A dedicated AWS client is created for Turbo only when both TURBO_AWS_REGION and TURBO_AWS_ENDPOINT are set; otherwise the default AWS client (AWS_REGION/AWS_ENDPOINT) is reused. |
| TURBO_AWS_ENDPOINT | String | undefined | Custom endpoint for the Turbo S3 data source. A dedicated AWS client is created for Turbo only when both TURBO_AWS_REGION and TURBO_AWS_ENDPOINT are set; otherwise the default AWS client (AWS_REGION/AWS_ENDPOINT) is reused. |
| TURBO_AWS_ACCESS_KEY_ID | String | undefined | Access key ID for the dedicated Turbo AWS client. Falls back to AWS_ACCESS_KEY_ID when unset. Only used when the dedicated Turbo client is created (TURBO_AWS_REGION + TURBO_AWS_ENDPOINT). |
| TURBO_AWS_SECRET_ACCESS_KEY | String | undefined | Secret access key for the dedicated Turbo AWS client. Falls back to AWS_SECRET_ACCESS_KEY when unset. Only used when the dedicated Turbo client is created (TURBO_AWS_REGION + TURBO_AWS_ENDPOINT). |
| TURBO_AWS_SESSION_TOKEN | String | undefined | Session token for the dedicated Turbo AWS client. Falls back to AWS_SESSION_TOKEN when unset. Only used when the dedicated Turbo client is created (TURBO_AWS_REGION + TURBO_AWS_ENDPOINT). |
| AWS_S3_CONTIGUOUS_DATA_BUCKET | String | undefined | AWS S3 bucket name used for storing data |
| AWS_S3_CONTIGUOUS_DATA_PREFIX | String | undefined | Prefix for the S3 bucket to organize data |
| CHUNK_POST_MIN_SUCCESS_COUNT | String | "3" | Minimum count of accepting responses (HTTP 200 or 303) for a given chunk to be considered properly seeded |
| CHUNK_POST_MIN_PREFERRED_SUCCESS_COUNT | String | "2" | Minimum count of accepting responses (HTTP 200 or 303) from preferred (tip) nodes for a chunk to be considered properly seeded. Set to 0 to disable preferred node requirement |
| CHUNK_POST_MAX_CONSECUTIVE_FAILURES | String | "5" | Maximum consecutive failures before stopping a chunk broadcast. Counts a run of 4xx rejections (only while no peer has accepted the chunk) or, once CHUNK_POST_CONTINUE_PAST_THRESHOLD is enabled and both success thresholds are already met, a run of connection failures/timeouts that bounds the extra post-threshold propagation (the dead peer tail). Set to 0 to disable early termination |
| CHUNK_POST_CONTINUE_PAST_THRESHOLD | Boolean | false | If true, keep broadcasting a chunk to every selected peer even after the success threshold is met, maximizing propagation/redundancy instead of stopping early. The success threshold still governs the response returned to the uploader; the dead peer tail is still bounded by CHUNK_POST_MAX_CONSECUTIVE_FAILURES. Because each broadcast then posts to the full peer set, pair this with a higher CHUNK_POST_PEER_CONCURRENCY (e.g. 20+) so broadcasts complete promptly instead of serializing through every peer |
| ARWEAVE_POST_DRY_RUN | Boolean | false | If true, simulates transaction header and chunk submission without posting to Arweave. POST /tx and POST /chunk return 200 OK as if successful; only the final network broadcast is skipped. Works on both port 3000 (Envoy) and port 4000 (direct). By default, transaction signatures and chunk merkle proofs are still validated before success. When disabled, Envoy routes these requests to trusted Arweave nodes instead; GET /tx is always proxied to the trusted node regardless of this setting. |
| ARWEAVE_POST_DRY_RUN_SKIP_VALIDATION | Boolean | false | If true (and ARWEAVE_POST_DRY_RUN is enabled), skips transaction signature verification and chunk merkle proof validation for faster testing. |
| ARWEAVE_PEER_DNS_RECORDS | String | "peers.arweave.xyz" | Comma-separated DNS hostnames to resolve for Arweave peer discovery. Set to empty string to disable and fall back to static TRUSTED_NODE_HOST/FALLBACK_NODE_HOST |
| ARWEAVE_PEER_DNS_PORT | Number | 1984 | Port to use when connecting to discovered Arweave peers |
| ARWEAVE_NODE_MAX_HEIGHT_LAG | Number | 5 | Maximum number of blocks a peer can be behind the reference height before being excluded |
| ARWEAVE_NODE_MAX_HEIGHT_LEAD | Number | 5 | Maximum number of blocks a peer can be ahead of the reference height before being excluded |
| ARWEAVE_HEIGHT_MIN_CONSENSUS_COUNT | Number | 2 | Minimum number of peers that must agree on a height (within MAX_HEIGHT_LAG) for consensus |
| ARWEAVE_NODE_FULL_SYNC_THRESHOLD | Number | 100 | Maximum gap between blocks and height+1 for a peer to be classified as a full node |
| ARWEAVE_PEER_HEALTH_CHECK_INTERVAL_MS | Number | 30000 | Interval in milliseconds between peer health check cycles |
| ENABLE_ARWEAVE_PEER_EDS | Boolean | true | Enable Envoy EDS-based dynamic peer routing (Docker/Envoy only). Set to false to use static trusted node clusters |
| CHUNK_DATA_RETRIEVAL_ORDER | String | "arweave-network,ar-io-network" | Comma-separated chunk data source order. Valid: arweave-network (Arweave miners/nodes), ar-io-network (AR.IO gateway peers), legacy-s3. Miners lead because they are protocol-incentivized to store arbitrary chunks, while gateways only hold what they happened to cache — asking the gateway layer first produces near-certain misses that concentrate on whichever gateway originated the data. ar-io-network remains as a fallback for chunks the miner set cannot serve |
| CHUNK_METADATA_RETRIEVAL_ORDER | String | "arweave-network,ar-io-network" | Comma-separated chunk metadata source order. Valid: arweave-network, ar-io-network, legacy-psql. Ordered miners-first for the same reason as CHUNK_DATA_RETRIEVAL_ORDER. legacy-psql requires LEGACY_PSQL_CONNECTION_STRING |
| CHUNK_DATA_SOURCE_PARALLELISM | Number | 1 | How many sources from CHUNK_DATA_RETRIEVAL_ORDER are tried in parallel. 1 = strict sequential fallback |
| CHUNK_METADATA_SOURCE_PARALLELISM | Number | 1 | How many sources from CHUNK_METADATA_RETRIEVAL_ORDER are tried in parallel. 1 = strict sequential fallback |
| CHUNK_GET_BASE64_SIZE_BYTES | Number | 368640 | Assumed size in bytes for base64-encoded chunk responses, used for x402 payment and rate limiting calculations (default: 360 KiB) |
| CHUNK_REQUEST_CONCURRENCY | Number | 50 | Maximum number of concurrent chunk fetch requests across all HTTP requests. Limits load on chunk backends under high concurrency |
| CHUNK_FIRST_DATA_TIMEOUT_MS | Number | 10000 | Timeout in milliseconds for receiving the first chunk when assembling data from chunks. If exceeded, the request fails and falls through to alternative data sources. 0 disables |
| TX_CHUNKS_GEOMETRY_DB_ENABLED | Boolean | true | Resolve chunk-read geometry (data_root, offset, size) for TxChunksDataSource from the local stable transactions index before the trusted node. Each cold range read otherwise costs two trusted-node requests, which share a 5 req/s budget with the transaction offset importer. Falls back to the chain on a local miss, and re-checks against the chain (once per transaction per hour) when a read using local geometry fails |
| TX_CHUNKS_GEOMETRY_CACHE_SIZE | Number | 10000 | Maximum entries in the in-memory cache of locally resolved chunk-read geometry. Entries are immutable (stable transactions only) |
| CHUNK_SERVE_DEADLINE_MS | Number | 12000 | Wall-clock deadline (ms) for serving a single chunk request (/chunk/:offset, /chunk/:offset/data). The cascade's per-source timeouts are additive with no overall ceiling, so this bounds the whole serve below the upstream proxy cut (~15s), returning promptly (a 404, matching the timeout-as-not-found contract) and releasing the connection. Reason is exposed via the chunk_serve_deadline_exceeded_total metric. Must stay below the proxy read timeout. 0 disables |
| CHUNK_PEER_ORIGIN_DEADLINE_MS | Number | 3000 | Wall-clock deadline (ms) for a chunk request forwarded by another gateway (X-AR-IO-Hops >= 1), applied instead of CHUNK_SERVE_DEADLINE_MS. That caller times out after 1s (PEER_REQUEST_TIMEOUT_MS) and falls back to its own sources, so a longer retrieval is delivered to nobody while still holding a libuv thread and the disk. Default is above the caller's 1s on purpose: a nearly-complete fetch still populates the cache, and 1s is this implementation's timeout rather than a protocol guarantee. 0 falls back to CHUNK_SERVE_DEADLINE_MS. Hits are counted by chunk_peer_origin_deadline_exceeded_total, but only while this deadline is the one in force: under 0 a forwarded request that runs out of time is recorded by chunk_serve_deadline_exceeded_total alone |
| CHUNK_PEER_ORIGIN_MODE | String | "off" | How to treat chunk requests that arrive with X-AR-IO-Hops >= 1, meaning another AR.IO gateway forwarded them. off runs the full cascade for peers, as today. enforce answers them using only sources this gateway can reach without asking the network (its local caches, its own index, operator-owned storage), declining AR.IO peers, Arweave nodes and the peer chunk-metadata anchor probe, and returning the usual not-found 404 when none of those can serve. audit changes nothing and records what enforcing would have cost via chunk_peer_origin_audit_total{boundary,bytes}. Any other value fails startup. The naming follows Kubernetes PodSecurity admission (enforce/audit). The requesting gateway allows one second before it gives up and runs its own cascade, so remote work started on its behalf outlives its patience; the AR.IO peer sources already bound forwarding depth with validateHopCount, but the boundary lookup and the Arweave node path do not. Run audit before enforce on any gateway that originates data and check the bytes="local" cells, which count requests this gateway served from its own disk. boundary="local",bytes="local" stays served under enforce; boundary="remote",bytes="local" does not, because the bytes were locatable only through a network offset lookup that enforce declines. A chunk ingested at upload time has no absolute-offset index entry, so it is reachable only once a boundary lookup resolves it. Detection depends on the caller sending the hop header, so non-AR.IO clients read as hops=0 and are unaffected |
| CHUNK_REBROADCAST_SOURCES | String | "" | Comma-separated list of sources that trigger chunk rebroadcasting. Valid: legacy-s3, ar-io-network, arweave-network. Empty disables rebroadcasting |
| CHUNK_REBROADCAST_RATE_LIMIT_TOKENS | Number | 60 | Rate limit tokens per interval for chunk rebroadcasting |
| CHUNK_REBROADCAST_RATE_LIMIT_INTERVAL | String | "minute" | Rate limit interval for chunk rebroadcasting: second, minute, hour, day |
| CHUNK_REBROADCAST_MAX_CONCURRENT | Number | 5 | Maximum concurrent chunk rebroadcast operations |
| CHUNK_REBROADCAST_DEDUP_TTL_SECONDS | Number | 3600 | Deduplication cache TTL in seconds (don't rebroadcast same chunk within this period) |
| CHUNK_REBROADCAST_MIN_SUCCESS_COUNT | Number | 1 | Minimum broadcast success count for chunk to be added to dedup cache |
| CHUNK_INGEST_CACHE_ENABLED | Boolean | false | When true, POST /chunk verifies each posted chunk against its data_root and write-through caches it locally (optimistic ingest caching). Default false = relay only (current behavior). The cache write is fire-and-forget and never delays the broadcast response |
| CHUNK_INGEST_CACHE_ALLOWLIST | String | "" | Comma-separated IPs/CIDRs whose posted chunks are eligible for optimistic caching. Empty = open ingest (any merkle-valid chunk is cached). When set, only these posters earn caching; others are still relayed. Gates caching, not posting. Matched against the poster's client address only (see TRUSTED_PROXIES). Moot under the default (open ingest, empty allowlist) since every merkle-valid chunk is cached regardless. |
| CHUNK_INGEST_CONFIRMATION_TIMEOUT_SECONDS | Number | 21600 | GC leash (seconds) for open-ingest chunks whose data_root never confirms on-chain. Must exceed worst-case mine+index latency |
| CHUNK_INGEST_ALLOWLIST_CONFIRMATION_TIMEOUT_SECONDS | Number | 86400 | Longer GC leash (seconds) for allowlisted-poster chunks |
| CHUNK_INGEST_MAX_PENDING_BYTES | Number | 26843545600 (25 GiB) | Hard cap on pending (unconfirmed) ingest-cached bytes. Enforced synchronously at ingest (further caching is rejected with chunk_ingest_cache_total{result="skipped_disk_full"} once the pending total would exceed it — a burst can't overrun the disk between GC sweeps) AND as a GC backstop (oldest-pending evicted first). Non-zero by default so enabling caching can't silently fill the disk. Set 0 to disable |
| CHUNK_INGEST_GC_INTERVAL_MS | Number | 300000 | Interval (ms) between chunk-ingest GC sweeps |
| CHUNK_INGEST_GC_BATCH_SIZE | Number | 1000 | Maximum placements evicted per GC sweep pass |
| CHUNK_INGEST_CONFIRMED_ROOT_RETENTION_SECONDS | Number | 3600 | How long a confirmed_data_roots sticky-confirmation marker is retained before the GC prunes it. A marker only needs to bridge the gap between a tx confirming on-chain and its chunks being seeded, so this can be short; keeps the marker table bounded |
| BUNDLE_REPAIR_RETRY_INTERVAL_SECONDS | String | "300" | Interval in seconds for retrying bundles |
| BUNDLE_REPAIR_RETRY_BATCH_SIZE | String | "5000" | Batch size for retrying bundles |
| BUNDLE_REPAIR_MAX_RETRY_ATTEMPTS | String | "50" | Max times the repair loop retries a single bundle before dropping it from the retry pool. Bounds re-walks of bundles that can never finish unbundling (the future dead-letter-queue hand-off boundary) |
| BUNDLE_REPAIR_RETRY_COOLDOWN_SECONDS | String | "900" | Minimum seconds between retries of the same bundle, keyed on last_retried_at so it holds even when the unbundler is saturated (unlike the last_queued_at reprocess cooldown) |
| APEX_TX_ID | String | undefined | If set, serves this transaction ID's data at the root path (/). Cache-Control on the response is bounded by CACHE_APEX_MAX_AGE with must-revalidate so apex rotations propagate to upstream caches predictably |
| APEX_ARNS_NAME | String | undefined | If set, resolves and serves this ArNS name's content at the root path (/). Supports comma-separated values positionally mapped to ARNS_ROOT_HOST entries (e.g., turbo,ar-io for two hosts). A single value applies to all hosts. |
| ENABLE_RATE_LIMITER | Boolean | false | If true, enables rate limiting enforcement (returns 429 when limits exceeded). When false, limits are tracked but not enforced |
| RATE_LIMITER_TYPE | String | "redis" (docker), "memory" (standalone) | Sets the rate limiter implementation type. Use "memory" for local development/single-node, "redis" for production multi-node deployments |
| RATE_LIMITER_REDIS_ENDPOINT | String | "redis://redis:6379" | Redis endpoint URL for rate limiter (only used when RATE_LIMITER_TYPE is "redis") |
| RATE_LIMITER_REDIS_USE_TLS | Boolean | false | Whether to use TLS when connecting to Redis for rate limiting |
| RATE_LIMITER_REDIS_USE_CLUSTER | Boolean | false | Whether to use Redis cluster mode for rate limiting |
| RATE_LIMITER_RESOURCE_TOKENS_PER_BUCKET | Number | 1000000 | Maximum tokens in the resource bucket (1 token = 1 KiB, where 1 KiB = 1,024 bytes). Default allows ~1 GiB per resource |
| RATE_LIMITER_RESOURCE_REFILL_PER_SEC | Number | 100 | Tokens to refill per second for resource bucket. Default allows ~100 KiB/s sustained throughput per resource |
| RATE_LIMITER_IP_TOKENS_PER_BUCKET | Number | 100000 | Maximum tokens in the IP bucket (1 token = 1 KiB, where 1 KiB = 1,024 bytes). Default allows ~100 MiB per IP |
| RATE_LIMITER_IP_REFILL_PER_SEC | Number | 20 | Tokens to refill per second for IP bucket. Default allows ~20 KiB/s sustained throughput per IP |
| TRUSTED_PROXIES | String | 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,169.254.0.0/16,::1/128,fc00::/7,fe80::/10 | Comma-separated IPs or CIDRs of proxies whose X-Forwarded-For and X-Real-IP are believed when working out the client address for rate limits, x402 and allowlists. Behind one, the client is the nearest X-Forwarded-For hop that is not itself a trusted proxy; anything else is identified by its own address, whatever its headers say. Envoy, on the Docker network, is trusted and appends the address that connected to it. Add a CDN's or public load balancer's ranges when one is in front, and any proxy on another public host, or every client behind it shares its address. none trusts no proxy: only for a core that clients reach directly, with no Envoy in front. With it, every request that comes through Envoy (the standard path, port 3000) carries Envoy's address, so all of them share one bucket. A malformed entry stops the gateway at startup |
| RATE_LIMITER_IPS_AND_CIDRS_ALLOWLIST | String | "" | Comma-separated IPs and CIDR ranges exempt from rate limiting and x402 payment (e.g., "192.168.1.0/24,10.0.0.1"). Matched against the client address only (see TRUSTED_PROXIES), never other addresses in its headers |
| RATE_LIMITER_ARNS_ALLOWLIST | String | "" | Comma-separated list of ArNS names to exempt from rate limiting and payment verification (e.g., "my-free-app,public-docs") |
| CACHE_PRIVATE_SIZE_THRESHOLD | Number | 104857600 (100 MB) | Response size threshold in bytes above which Cache-Control uses 'private' directive. Helps CDNs respect rate limiting and x402 payment requirements for large responses |
| CACHE_PRIVATE_CONTENT_TYPES | String | "" | Comma-separated list of content types that should use 'private' Cache-Control directive. Supports wildcards (e.g., "image/,video/"). Helps CDNs respect rate limiting and x402 payments for specific content types |
| CACHE_DEFAULT_MAX_AGE | Number | 30 | Default Cache-Control max-age (seconds) applied by middleware when no handler sets its own header |
| CACHE_STABLE_MAX_AGE | Number | 2592000 (30 days) | Cache-Control max-age (seconds) for stable (deeply confirmed) data. Warning: values larger than your longest expected ANT TTL amplify upstream-cache poisoning when an ArNS record or manifest is updated — see note below |
| CACHE_UNSTABLE_TRUSTED_MAX_AGE | Number | 43200 (12 hours) | Cache-Control max-age (seconds) for unstable data from a trusted source. Warning: see CACHE_STABLE_MAX_AGE note below |
| CACHE_UNSTABLE_MAX_AGE | Number | 7200 (2 hours) | Cache-Control max-age (seconds) for unstable data from an untrusted source. Warning: see CACHE_STABLE_MAX_AGE note below |
| CACHE_NOT_FOUND_MAX_AGE | Number | 60 (1 minute) | Cache-Control max-age (seconds) for not-found responses, manifest fallback responses, and ArNS custom-404 responses (ARNS_NOT_FOUND_TX_ID / ARNS_NOT_FOUND_ARNS_NAME) |
| CACHE_APEX_MAX_AGE | Number | 3600 (1 hour) | Cache-Control max-age (seconds) for APEX_TX_ID responses. Bounds the upstream-cache poisoning window when operators rotate APEX_TX_ID (lower than the data-layer ladder so a rotation propagates within the configured window) |
| ENABLE_X_402_USDC_DATA_EGRESS | Boolean | false | If true, enables x402 USDC payment verification and settlement for data egress |
| X_402_USDC_NETWORK | String | "base-sepolia" | USDC network to use ("base" for mainnet, "base-sepolia" for testnet). On base, working CDP credentials are required: without them the gateway falls back to X_402_USDC_FACILITATOR_URL, and the commonly configured facilitators there support testnets only, so every payment fails verification. A startup warning is emitted in that case |
| X_402_USDC_WALLET_ADDRESS | String | undefined | Ethereum wallet address (0x...) to receive USDC payments |
| X_402_USDC_FACILITATOR_URL | String | "https://x402.org/facilitator" | x402 facilitator endpoint URL. Default is Coinbase's testnet facilitator. Note: When CDP API keys are provided (CDP_API_KEY_ID and CDP_API_KEY_SECRET), the Coinbase facilitator is automatically used, overriding this setting |
| X_402_USDC_DATA_EGRESS_MIN_PRICE | Number | 0.001 | Minimum price in USDC for data egress (used when content length is unknown) |
| X_402_USDC_DATA_EGRESS_MAX_PRICE | Number | 1.00 | Maximum price in USDC for data egress (caps per-request cost) |
| X_402_USDC_PER_BYTE_PRICE | Number | 0.0000000001 | Price in USDC per byte of data egress (default: $0.10 per GB) |
| X_402_RATE_LIMIT_CAPACITY_MULTIPLIER | Number | 10 | Capacity multiplier for paid tier rate limits (e.g., 10x = paid users get 10x bucket capacity) |
| X_402_USDC_SETTLE_TIMEOUT_MS | Number | 5000 | Timeout in milliseconds for payment settlement operations |
| X_402_CDP_CLIENT_KEY | String | undefined | Coinbase Developer Platform client API key (public, safe to expose in browser paywall). Paywall only — this is NOT a facilitator credential. Facilitator auth uses CDP_API_KEY_ID + CDP_API_KEY_SECRET; supplying the client key there makes CDP reject every payment with 401, which surfaces only as a generic verification failure. It is accepted as a fallback for pre-existing workarounds, with a startup warning |
| X_402_APP_NAME | String | "AR.IO Gateway" | Application name displayed in payment UI |
| X_402_APP_LOGO | String | undefined | URL to application logo displayed in payment UI |
| X_402_SESSION_TOKEN_ENDPOINT | String | undefined | Custom session token endpoint URL for payment authentication |
| CDP_API_KEY_ID | String | undefined | SENSITIVE SECRET: Coinbase Developer Platform secret API key ID (for Onramp session token generation and Coinbase facilitator integration). When provided with CDP_API_KEY_SECRET, automatically enables Coinbase facilitator. Must not be logged or exposed. Store in secure secrets manager and restrict access (least privilege). |
| CDP_API_KEY_SECRET | String | undefined | SENSITIVE SECRET: Coinbase Developer Platform secret API key secret (for Onramp session token generation and Coinbase facilitator integration). When provided with CDP_API_KEY_ID, automatically enables Coinbase facilitator. Must not be logged or exposed. Store in secure secrets manager and restrict access (least privilege). |
| CDP_API_KEY_SECRET_FILE | String | undefined | SENSITIVE SECRET: File path containing CDP secret API key secret. Takes precedence over CDP_API_KEY_SECRET if defined. Must not be logged or exposed. |
| OTEL_SERVICE_NAME | String | "ar-io-node" | Service name for OpenTelemetry traces. Used to identify this service in telemetry backends |
| OTEL_TRACING_SAMPLING_RATE_DENOMINATOR | Number | 1 | Head-based sampling rate denominator (1/N spans sampled). Note: In docker-compose, tail sampling via OTEL Collector is used instead for intelligent sampling |
| OTEL_EXPORTER_OTLP_ENDPOINT | String | unset (no OTLP export) | OTLP endpoint for traces and logs, e.g. http://otel-collector:4318 for the compose collector. Unset, nothing is exported over OTLP |
| OTEL_EXPORTER_OTLP_HEADERS | String | undefined | Authentication headers for OTLP exporter (e.g., "x-honeycomb-team=your-api-key"). For non-docker deployments only. Use OTEL_COLLECTOR_DESTINATION_HEADERS in docker |
| OTEL_EXPORTER_OTLP_HEADERS_FILE | String | undefined | File path containing OTLP exporter headers. For non-docker deployments only. Use OTEL_COLLECTOR_DESTINATION_HEADERS_FILE in docker |
| OTEL_FILE_EXPORT_ENABLED | Boolean | false (true with yarn service:start) | Enable file-based export of OTEL spans for development/debugging. Spans written to OTEL_FILE_EXPORT_PATH |
| OTEL_FILE_EXPORT_PATH | String | "logs/otel-spans.jsonl" | Path for file-based OTEL span export (JSONL format). Only used when OTEL_FILE_EXPORT_ENABLED is true |
| OTEL_BATCH_LOG_PROCESSOR_SCHEDULED_DELAY_MS | Number | 2000 | Delay in milliseconds before batch log export |
| OTEL_BATCH_LOG_PROCESSOR_MAX_EXPORT_BATCH_SIZE | Number | 10000 | Maximum number of log records to export in a single batch |
| OTEL_COLLECTOR_IMAGE_TAG | String | "0.119.0" | Docker image tag for OpenTelemetry Collector. Only applies to docker-compose deployments |
| OTEL_COLLECTOR_DESTINATION_ENDPOINT | String | undefined | Final telemetry destination URL where OTEL Collector forwards sampled traces. Examples: Honeycomb (https://api.honeycomb.io), Grafana Cloud, Datadog, New Relic, Elastic APM |
| OTEL_COLLECTOR_HONEYCOMB_API_KEY | String | undefined | Honeycomb API key for authentication (x-honeycomb-team header). Configure ONE backend API key |
| OTEL_COLLECTOR_GRAFANA_CLOUD_API_KEY | String | undefined | Grafana Cloud API key for authentication (Authorization: Basic header). Base64 encoded instance_id:api_key. Configure ONE backend API key |
| OTEL_COLLECTOR_DATADOG_API_KEY | String | undefined | Datadog API key for authentication (DD-API-KEY header). Configure ONE backend API key |
| OTEL_COLLECTOR_NEW_RELIC_API_KEY | String | undefined | New Relic license key for authentication (api-key header). Configure ONE backend API key |
| OTEL_COLLECTOR_ELASTIC_API_KEY | String | undefined | Elastic APM secret token for authentication (Authorization: Bearer header). Configure ONE backend API key |
| OTEL_TAIL_SAMPLING_SUCCESS_RATE | Number | 1 | Percentage (1-100) of successful/fast/unpaid traces to sample. Default: 1% provides baseline metrics with 80-95% cost reduction |
| OTEL_TAIL_SAMPLING_SLOW_THRESHOLD_MS | Number | 2000 | Latency threshold in milliseconds. Requests exceeding this duration are eligible for sampling (rate controlled by OTEL_TAIL_SAMPLING_SLOW_RATE) |
| OTEL_TAIL_SAMPLING_ERROR_RATE | Number | 100 | Percentage (1-100) of error traces (5xx status codes) to sample. Default: 100% captures all errors for debugging. Lower values reduce costs but may miss issues |
| OTEL_TAIL_SAMPLING_SLOW_RATE | Number | 100 | Percentage (1-100) of slow request traces (exceeding OTEL_TAIL_SAMPLING_SLOW_THRESHOLD_MS) to sample. Default: 100% captures all slow requests for performance analysis |
| OTEL_TAIL_SAMPLING_PAID_TRAFFIC_RATE | Number | 100 | Percentage (1-100) of paid traffic traces (x402 verified payments) to sample. Default: 100% for billing/compliance. Reduce only if paid traffic is high volume |
| OTEL_TAIL_SAMPLING_PAID_TOKENS_RATE | Number | 100 | Percentage (1-100) of paid rate limit token usage traces to sample. Default: 100% for billing/compliance. Reduce only if paid token usage is high volume |
| OTEL_TAIL_SAMPLING_NESTED_BUNDLE_RATE | Number | 5 | Percentage (1-100) of nested bundle data item retrieval traces to sample. Captures TurboDynamoDB requests involving parent offsets for visibility into nested bundle paths |
| OTEL_TAIL_SAMPLING_OFFSET_OVERWRITE_RATE | Number | 10 | Percentage (1-100) of offset overwrite risk traces to sample. Captures traces where both DynamoDB offsets AND raw data paths executed, the scenario that triggered the Release 59 offset bug |
Security Note: Variables marked as SENSITIVE SECRET (such as CDP_API_KEY_ID, CDP_API_KEY_SECRET, and CDP_API_KEY_SECRET_FILE) contain confidential credentials that must never be printed to logs, exposed in error messages, or included in any diagnostic output. Always mask or omit these values in logs, store them in a secure secrets manager, and restrict access using the principle of least privilege.
The gateway reads protocol state from Solana on-chain programs. The
OnDemandArNSResolver routes ANT lookups through
@ar.io/sdk/solana::SolanaANTReadable.
| Variable | Type | Default | Description |
|---|---|---|---|
SOLANA_RPC_URL |
String | "https://api.mainnet-beta.solana.com" |
Solana RPC endpoint. Public mainnet-beta is rate-limited; use a dedicated provider (Helius, QuickNode, Triton) for production. For devnet: https://api.devnet.solana.com. |
ARIO_CORE_PROGRAM_ID |
String | the mainnet ID in docker-compose.yaml; unset outside Docker (the SDK uses mainnet) |
Override the ario-core program ID. Required for devnet / localnet (current staging-devnet value: 5iU1xZ4ocy7e96kcoEipvnxs8anSoq6JGznq6iS4svKn). Source of truth: DEVNET_PROGRAM_IDS in @ar.io/sdk src/solana/clusters.ts / devnet-config.json in ar-io/solana-ar-io. |
ARIO_GAR_PROGRAM_ID |
String | the mainnet ID in docker-compose.yaml; unset outside Docker |
Override the ario-gar program ID. Staging devnet: KpZMWCMeTiyH3dW3ZH9go4TwAg5vxgUXHuFVY8JbLFS. |
ARIO_ARNS_PROGRAM_ID |
String | the mainnet ID in docker-compose.yaml; unset outside Docker |
Override the ario-arns program ID. Staging devnet: 2YjqZEYTTKLD3qg4NvYwpo2wVcDQCj2p4iD2WTYymfEC. |
ARIO_ANT_PROGRAM_ID |
String | the mainnet ID in docker-compose.yaml; unset outside Docker |
Override the ario-ant program ID. Staging devnet: 9SuQQKKW1mEvdRhXrdpHR5PqBMurY3wh7vbEkxzEsngu. |
AR.IO gateways have four protocol-level roles that can be wired to one or more
wallets. The on-chain roles (operator, observer) must be Solana keypairs; the
off-chain upload role can use any of three chains that Turbo accepts. Resolution
lives in ar-io-observer/src/wallet-config.ts (covered by wallet-config.test.ts).
operator (= cranker) = SOLANA_KEYPAIR_PATH (required)
observer (save_observations) = OBSERVER_KEYPAIR_PATH ?? SOLANA_KEYPAIR_PATH
upload (precedence) = Arweave > Ethereum > Solana(explicit) > Solana(fallback)
Upload precedence (in order, first match wins):
ARWEAVE_UPLOAD_KEY_FILE(file) →ArweaveSignerARWEAVE_UPLOAD_JWK(inline env) →ArweaveSignerETHEREUM_UPLOAD_PRIVATE_KEY_FILE(file) →EthereumSignerETHEREUM_UPLOAD_PRIVATE_KEY(inline env) →EthereumSignerSOLANA_UPLOAD_KEYPAIR_PATH(explicit) →SolanaSigner- Fallback:
OBSERVER_KEYPAIR_PATH ?? SOLANA_KEYPAIR_PATH→SolanaSigner
Conflict policy: setting envs from more than one chain group at once
(e.g. ARWEAVE_UPLOAD_* plus ETHEREUM_UPLOAD_*) raises a startup error
listing every conflicting env. Pick exactly one upload chain.
Sniff validators produce friendly errors when material is dropped into the
wrong slot — e.g. a Solana keypair JSON at ARWEAVE_UPLOAD_KEY_FILE, an Arweave
JWK at SOLANA_UPLOAD_KEYPAIR_PATH, a 32-byte hex string at a keypair path, etc.
Each error names the offending env and suggests the correct one.
Supported configurations (each tested in wallet-config.test.ts):
| # | Operator | Observer | Upload | Required envs |
|---|---|---|---|---|
| 1 | Solana | =op | =op (Solana bundle) | SOLANA_KEYPAIR_PATH |
| 2 | Solana | =op | Arweave JWK | SOLANA_KEYPAIR_PATH + ARWEAVE_UPLOAD_KEY_FILE |
| 3 | Solana A | Solana B | Solana C (bundle) | SOLANA_KEYPAIR_PATH + OBSERVER_KEYPAIR_PATH + SOLANA_UPLOAD_KEYPAIR_PATH |
| 4 | Solana A | Solana B | Arweave JWK | SOLANA_KEYPAIR_PATH + OBSERVER_KEYPAIR_PATH + ARWEAVE_UPLOAD_KEY_FILE |
| 5 | Solana A | Solana B | Ethereum | SOLANA_KEYPAIR_PATH + OBSERVER_KEYPAIR_PATH + ETHEREUM_UPLOAD_PRIVATE_KEY_FILE |
| Variable | Type | Default | Description |
|---|---|---|---|
SOLANA_KEYPAIR_PATH |
Path | - | Operator + cranker keypair. Required in Solana mode. Signs join_network, update_gateway_settings, and every permissionless cranker ix. |
OBSERVER_KEYPAIR_PATH |
Path | - | Optional separate observer keypair. Signs save_observations. When set, must match the on-chain Gateway.observer_address (settable at join time via --observer-address or later via update_observer_address). Falls back to SOLANA_KEYPAIR_PATH. |
SOLANA_UPLOAD_KEYPAIR_PATH |
Path | - | Optional separate Solana keypair for report uploads. Produces an arbundles.SolanaSigner. Falls back to OBSERVER_KEYPAIR_PATH ?? SOLANA_KEYPAIR_PATH. Ignored when ARWEAVE_UPLOAD_* or ETHEREUM_UPLOAD_* is set. |
ARWEAVE_UPLOAD_KEY_FILE |
Path | - | Path to an Arweave JWK used for report uploads (arbundles.ArweaveSigner). Top priority. |
ARWEAVE_UPLOAD_JWK |
String | - | Inline Arweave JWK JSON. Lower priority than ARWEAVE_UPLOAD_KEY_FILE. |
ETHEREUM_UPLOAD_PRIVATE_KEY_FILE |
Path | - | Path to a 32-byte hex Ethereum private key (with or without 0x prefix). Produces an arbundles.EthereumSigner. Lower priority than Arweave; higher than Solana. |
ETHEREUM_UPLOAD_PRIVATE_KEY |
String | - | Inline Ethereum private key (hex). Lower priority than the file form. |
save_observations submission flow (Solana mode, SUBMIT_CONTRACT_INTERACTIONS=true):
- The
OnDemandArNSResolver+ continuous observer produce aObserverReportfor the current epoch. TurboReportSinkuploads the report bundle to permaweb under the upload identity, returning the 43-char Arweave TX ID.SolanaContractReportSinkreads the current Epoch account once (pre-flight gate). It skips submission if:- The observer's pubkey isn't in
epoch.prescribed_observersfor this epoch (we weren't picked this round). - The
has_observedbit at our observer slot is already set (we already submitted). now >= epoch.end_timestamp(the observation window has closed).
- The observer's pubkey isn't in
- Otherwise it builds the
save_observationsinstruction with the failed-gateway bitmap + base64url-decoded report TX ID and submits it signed by the observer keypair (not the operator/cranker). - After the parent epoch is fully distributed, the cranker's
close_observationloop reclaims the Observation PDA's rent.
The on-chain Observation.report_tx_id field stores the raw 32-byte hash (lossless decode of the 43-char base64url txid) so consumers can recover the full Arweave txid for permaweb audits.
CACHE_STABLE_MAX_AGE, CACHE_UNSTABLE_TRUSTED_MAX_AGE, and
CACHE_UNSTABLE_MAX_AGE govern the Cache-Control: max-age returned for
data responses. CACHE_STABLE_MAX_AGE additionally emits the immutable
directive, which tells browsers and upstream proxies (e.g., nginx) not
to revalidate before the max-age expires.
URL → data-id bindings served via ArNS are mutable: ArNS records can be
updated, manifests can be republished, and a previously-fallback path may
become path-mapped in a future manifest revision. If a CACHE_*_MAX_AGE
value exceeds your longest expected ANT TTL, upstream proxies hold these
responses long after the underlying mapping has changed, producing stale
or broken pages — typically blank apps, missing images, or "domain-yours"
placeholders served on a now-active ArNS name.
Note that ARNS_NOT_FOUND_ARNS_NAME defaults to 'unregistered_arns', so
every gateway with default config routes failed ArNS resolutions
through this branch. As of PE-9072 these responses are explicitly bounded
to CACHE_NOT_FOUND_MAX_AGE regardless of CACHE_UNSTABLE_*_MAX_AGE; on
gateways running prior versions, lower CACHE_UNSTABLE_TRUSTED_MAX_AGE to
the default and sweep upstream caches for placeholder content.
Recommendations:
- Keep
CACHE_STABLE_MAX_AGE,CACHE_UNSTABLE_TRUSTED_MAX_AGE, andCACHE_UNSTABLE_MAX_AGEat their defaults unless you have an operational procedure for evicting poisoned upstream cache entries (e.g., grep nginx cache files for the placeholder'sX-AR-IO-Data-Idand remove matches). - Manifest fallback responses,
ARNS_NOT_FOUND_TX_ID/ARNS_NOT_FOUND_ARNS_NAMEresponses, and 404s are bounded toCACHE_NOT_FOUND_MAX_AGE(default 60s) withmust-revalidateregardless of the operator's other cache settings. APEX_TX_IDresponses are bounded toCACHE_APEX_MAX_AGE(default 1h) withmust-revalidateso an apex rotation propagates within the configured window.
The following environment variables configure the Observer service for network gateway observations.
These settings control which gateways are used as references for ArNS resolution and chunk verification checks.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| REFERENCE_GATEWAY_HOSTS | String | ARNS_ROOT_HOST (compose), else "turbo-gateway.com,ar-io.net" |
Comma-separated list of reference gateway hosts for ArNS and chunk checks. These gateways are tried in order before network fallback |
| REFERENCE_GATEWAY_NETWORK_ONLY | Boolean | false | If true, uses only network gateways for reference checks (no explicit hosts). Enables fully decentralized observation |
| REFERENCE_GATEWAY_NETWORK_FALLBACK | Boolean | true | If true, falls back to network consensus when explicit reference hosts fail or disagree |
| REFERENCE_GATEWAY_CONSENSUS_SIZE | Number | 3 | Number of network gateways to query when building consensus for ArNS resolution |
| REFERENCE_GATEWAY_CONSENSUS_THRESHOLD | Number | 2 | Minimum number of agreeing gateways required to establish consensus |
| REFERENCE_GATEWAY_MIN_PASS_RATE | Number | 0.8 | Minimum historical pass rate (0.0-1.0) for a network gateway to be eligible for consensus queries |
| REFERENCE_GATEWAY_MIN_CONSECUTIVE_PASSES | Number | 2 | Minimum consecutive passing epochs required for network gateway eligibility |
| REFERENCE_GATEWAY_MIN_EPOCH_COUNT | Number | 5 | Minimum total epochs observed for a network gateway to be considered for eligibility |
| REFERENCE_GATEWAY_MAX_NETWORK_POOL | Number | 10 | Maximum number of eligible network gateways to keep in the pool for random selection |
| REFERENCE_GATEWAY_NETWORK_CACHE_TTL_SECONDS | Number | 14400 | TTL in seconds for caching the list of eligible network gateways from the AR.IO contract |
| REFERENCE_GATEWAY_CONSENSUS_MAX_ATTEMPTS | Number | 2 | Maximum number of gateway queries to attempt when trying to reach consensus threshold |
These settings control the continuous observation mode, which spreads observations across the epoch window and uses majority voting for pass/fail determination.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| OBSERVATIONS_PER_GATEWAY | Number | 3 | Number of times each gateway is observed during an epoch. Results are aggregated using majority voting |
| OBSERVATION_WINDOW_FRACTION | Number | 0.5 | Fraction of the epoch window (0.1-0.9) during which observations are spread. Higher values spread observations over more time |
| OBSERVATION_CYCLE_INTERVAL_MS | Number | 30000 | Interval in milliseconds between observation cycles. Each cycle processes scheduled observations |
| MAJORITY_VOTE_THRESHOLD | Number | 2 | Number of passing observations needed for a gateway to pass overall. Should be ≤ OBSERVATIONS_PER_GATEWAY |
| GATEWAY_ASSESSMENT_CONCURRENCY | Number | 10 | Parallel gateway assessments per observation cycle (network-bound). Higher values speed assessment but add concurrent outbound load |
| NAME_ASSESSMENT_CONCURRENCY | Number | 5 | Parallel ArNS-name resolution checks per gateway during assessment |
| ENABLE_LOG_REPORT_SINK | Boolean | false | When true, enables the LogReportSink which logs full per-gateway assessment detail at info level |
These settings control the observer's offset (chunk-proof) checks. Each epoch the observer samples a share of gateways, chosen identically by every observer from the shared epoch entropy. For each sampled gateway it fetches /chunk/<offset> at up to OFFSET_SAMPLE_COUNT deterministic offsets and validates the returned Merkle proof (data_path) against the chain's data_root and transaction bounds. The check stops at the first offset that validates, so a gateway fails only when every offset fails. Timeouts, error statuses and invalid proofs all count as failures. If the observer could not judge any sample itself (its own reference or Arweave lookups failed), the assessment is reported as inconclusive and never fails the gateway, even with enforcement on. It runs on each of the gateway's observations and is majority-voted like the other checks.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| OFFSET_OBSERVATION_SAMPLE_RATE | Number | 0.20 | Share (0.0-1.0) of gateways sampled for offset checks each epoch. At least one gateway is sampled when the rate is above 0 |
| OFFSET_SAMPLE_COUNT | Number | 4 | Offsets tried per sampled gateway. The gateway passes on the first offset whose proof validates |
| OFFSET_OBSERVATION_ENABLED | Boolean | true | If true, enables offset checks. When false, no gateway is sampled and reports carry no offsetAssessments |
| OFFSET_OBSERVATION_ENFORCEMENT_ENABLED | Boolean | true | If true, a sampled gateway that fails its offset check fails the observation. When false, failures are recorded in the report but don't affect pass/fail |
These settings control whether Arweave transaction/data item tags and verification metadata are included as HTTP response headers when serving data. Note: for L2 data item signature and owner key headers, WRITE_ANS104_DATA_ITEM_DB_SIGNATURES must also be true.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| ARWEAVE_TAG_RESPONSE_HEADERS_ENABLED | Boolean | true | If true, includes transaction/data item tags as X-Arweave-Tag-* and verification headers (X-Arweave-Signature, X-Arweave-Owner, etc.) on /raw/:id and /:id responses. Also supplies the item's Content-Encoding tag when nothing recorded a coding for cached bytes; with it off, such an item is served without Content-Encoding |
| ARWEAVE_TAG_RESPONSE_HEADERS_MAX | Number | 100 | Maximum number of tag headers to include per response. If a transaction has more tags, an X-Arweave-Tags-Truncated: true header is added |
| ARWEAVE_TAG_RESPONSE_HEADERS_MAX_BYTES | Number | 8192 | Maximum total bytes for all emitted tag and verification headers. Prevents exceeding intermediary header size limits (nginx default 8KB). Verification headers are prioritized over tags |
| TX_METADATA_RESOLVE_CONCURRENCY | Number | 1 | Maximum number of concurrent background data item metadata resolutions. Limits resource pressure from remote fetches and DB writes when many uncached items are requested simultaneously |
When enabled, the transaction(id) GraphQL query can resolve unindexed data items on-demand by extracting metadata directly from ANS-104 bundle binaries. Only applies to single-ID lookups; the plural transactions(...) query is unaffected.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| GRAPHQL_ON_DEMAND_RESOLUTION_ENABLED | Boolean | true | If true, enables on-demand data item resolution as a fallback when transaction(id) returns null from the local database |
| GRAPHQL_ON_DEMAND_RESOLUTION_TIMEOUT_MS | Number | 5000 | Maximum time in milliseconds to wait for on-demand resolution before returning null. Background resolution continues and persists for future queries |
| GRAPHQL_ON_DEMAND_RESOLUTION_MAX_CONCURRENT | Number | 1 | Maximum number of concurrent on-demand resolution operations. When at capacity, requests return null immediately |
| GATEWAYS_GQL_URLS | JSON array of URLs | [] |
Fan-out GraphQL upstreams. When non-empty, the /graphql endpoint queries every listed gateway in parallel and merges the results. Assumes all upstreams use the ar-io-node cursor format. Empty disables the layer. |
| GATEWAYS_GQL_INCLUDE_LOCAL | Boolean | true | When GATEWAYS_GQL_URLS is set, include the local GqlQueryable (SQLite or Composite ClickHouse) as one more source in the merge. Set to false for a pure fan-out proxy. |
| GATEWAYS_GQL_REQUEST_TIMEOUT_MS | Number | 9500 | Per-upstream HTTP request timeout for fan-out GraphQL queries. Should be slightly less than GATEWAYS_GQL_CIRCUIT_BREAKER_TIMEOUT_MS so axios aborts first |
| GATEWAYS_GQL_CIRCUIT_BREAKER_TIMEOUT_MS | Number | 10000 | Per-upstream circuit breaker call timeout. An open breaker causes the fan-out to skip that upstream and continue with the rest |
| GATEWAYS_GQL_CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE | Number | 80 | Error rate (0–100) within the rolling window that trips the breaker open |
| GATEWAYS_GQL_CIRCUIT_BREAKER_ROLLING_COUNT_TIMEOUT_MS | Number | 60000 | Rolling window in milliseconds over which error rate is measured |
| GATEWAYS_GQL_CIRCUIT_BREAKER_RESET_TIMEOUT_MS | Number | 30000 | How long an open breaker stays open before transitioning to half-open for a trial request |
| GATEWAYS_GQL_SOFT_DEADLINE_ENABLED | Boolean | false | When true, fan-out list queries (transactions/blocks) stop waiting for slow upstreams once at least one has answered with results (an empty answer does not count, since the local index often answers first and empty) and GATEWAYS_GQL_SOFT_DEADLINE_MS elapses, returning a partial merge with an UPSTREAM_SOFT_DEADLINE warning per unfinished source. Abandoned requests keep running so the circuit breaker still sees their true outcome. A partial page can omit items from a cut source that sort before items it includes, so a client paging with cursors may skip them; the warning is the only signal. When false (default), the fan-out waits for every upstream (bounded by GATEWAYS_GQL_REQUEST_TIMEOUT_MS) |
| GATEWAYS_GQL_SOFT_DEADLINE_MS | Number | 2000 | Soft-deadline window in milliseconds, armed once the first upstream returns a non-empty result. Only used when GATEWAYS_GQL_SOFT_DEADLINE_ENABLED is true |
Signs gateway responses with an Ed25519 key per RFC 9421 (HTTP Message Signatures). Every qualifying response gets Signature and Signature-Input headers that cryptographically bind the gateway's trust claims (verification status, ArNS resolution, data item tags, published index files) to a staked on-chain identity.
When OBSERVER_KEYPAIR_PATH or OBSERVER_PRIVATE_KEY is set, the gateway uses the observer's Solana keypair directly as the HTTPSIG signing key. Verifiers derive the Solana address from the public key in the keyId and look it up in the on-chain GAR — no separate attestation document is needed. When neither is set, a standalone Ed25519 key is auto-generated (responses are signed but not verifiable against the on-chain registry). Setting both is rejected as ambiguous.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| HTTPSIG_ENABLED | Boolean | true | Enable RFC 9421 HTTP Message Signature signing on gateway responses |
| HTTPSIG_KEY_FILE | String | data/keys/httpsig.pem | Path to Ed25519 private key PEM file. Auto-generated on first startup if missing. Ignored when OBSERVER_KEYPAIR_PATH or OBSERVER_PRIVATE_KEY is set |
| KEYS_DATA_PATH | String | ./data/keys | Host path for the keys volume mount (docker-compose only). Maps to /app/data/keys inside the container |
| HTTPSIG_BIND_REQUEST | Boolean | true | Include @method;req and @path;req in signatures, binding each response to the specific request that triggered it |
| HTTPSIG_BODY_DIGEST_BUFFER_MAX_BYTES | Number | 2097152 (2 MiB) | Applies only to uncached data responses (/raw, /tx, ArNS-resolved, manifest-served). When > 0, uncached responses with a known body size at-or-below this byte threshold are buffered to compute SHA-256 and emit Content-Digest, which is in CO_SIGNABLE_HEADERS and therefore covered by the HTTPSIG signature. Cached and HEAD responses always emit the stored digest. Chunk responses (/chunk/:offset, /chunk/:offset/data) compute their digest inline from the in-memory 256 KiB chunk regardless of this setting and are not affected by it. Set to 0 to disable buffered emission for uncached small data responses; cached and chunk responses still emit Content-Digest. Non-numeric or negative values fall back to the default. Independent of this setting, X-AR-IO-Digest is emitted as an unsigned advisory header for any indexed transaction whose canonical hash is on file — including large uncached responses — so clients can verify served bytes against the chain value without an extra round-trip; it is overwritten by the actually-served-body hash when the buffered helper runs. |
| OBSERVER_KEYPAIR_PATH | String | - | Path to the observer's Solana keypair JSON file (64-byte array). When set, this key is used for HTTPSIG signing and its Solana address is verifiable in the on-chain GAR |
| OBSERVER_PRIVATE_KEY | String | - | Alternative to OBSERVER_KEYPAIR_PATH: a base58-encoded 64-byte Solana secret key (the format Phantom and other browser wallets export). Convenient for operators who don't already have a keypair JSON file on disk. Setting both OBSERVER_KEYPAIR_PATH and OBSERVER_PRIVATE_KEY is rejected as ambiguous |
Operator-defined TTL rules for data in ClickHouse. Reloaded from disk by
clickhouse-auto-import at the top of every import cycle. See
Parquet and ClickHouse usage
for the rules-file format and behavior.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| CLICKHOUSE_URL | String | undefined | ClickHouse HTTP endpoint the core service reads from, e.g. http://clickhouse:8123 under the bundled compose file. Unset disables the ClickHouse read path; GraphQL is served from SQLite instead |
| CLICKHOUSE_USER | String | undefined | ClickHouse username for the core service. Set explicitly whenever your ClickHouse uses an account other than default. Both the core and the importer fall back to default when unset, so the hazard is the importer and the core holding different effective credentials rather than one side defaulting differently. See the note below |
| CLICKHOUSE_PASSWORD | String | undefined | Password for CLICKHOUSE_USER |
| CLICKHOUSE_TTL_RULES_PATH | String | ./config/clickhouse-ttl-rules.yaml | Path to the YAML file of tag- and owner-based TTL rules loaded into ClickHouse before each import cycle |
A credential mismatch fails in a misleading way. The importer and the core connect separately:
clickhouse-auto-importis a bash script that defaults the user at the point of use (export CLICKHOUSE_USER="${CLICKHOUSE_USER:-default}"inscripts/lib/common.sh), while the core passes whatsrc/config.tsread into@clickhouse/client. Both land ondefaultwhen nothing is set, so an unset user is not itself a mismatch; the failure case is the two ending up with different effective credentials, for instance when only one environment carries the password for a non-default account.What makes it expensive is the shape of the failure rather than the failure itself. Imports keep succeeding, so ClickHouse looks healthy, the healthcheck stays green, and
blocks(...)and/raw/<id>keep working, while the stable ClickHousetransactionsleg fails. The gateway's own logs do not carry the reason, because the rejection is raised ClickHouse-side; depending on configuration you may seeFailed to read ClickHouse max height; skipping boundary optimization, which only appears when SQLite boundary optimization is enabled and is not necessarily reported as a timeout. Confirm against ClickHouse directly rather than inferring from the gateway:SELECT * FROM system.text_log WHERE message LIKE '%Authentication failed%' ORDER BY event_time DESC LIMIT 10;A
Code: 194entry naming a user you did not intend is the signature.
Controls the clickhouse-auto-import container that drives the Parquet
export → ClickHouse import → SQLite prune cycle. Each cycle walks every
batch from minStableDataItem up to maxStableDataItem, so cycle length
scales with both height range and per-batch wall-clock. See
ClickHouse pipeline → Cycle latency tuning
for the full latency breakdown and tradeoff discussion.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| CLICKHOUSE_AUTO_IMPORT_IMAGE_TAG | String | latest | Image tag for ghcr.io/ar-io/ar-io-clickhouse-auto-import. Pin to a SHA for reproducible deploys |
| CLICKHOUSE_AUTO_IMPORT_SLEEP_INTERVAL | Number | 3600 | Seconds to sleep between cycles |
| CLICKHOUSE_AUTO_IMPORT_HEIGHT_INTERVAL | Number | 10000 | Heights per batch. The dominant per-batch overhead is the parquet-export status poll (~5s) plus a few small HTTP calls, not the linear-in-rows export work — so larger batches amortize fixed cost across more heights. Bumping from 100 → 1000 typically cuts cycle time 5–10×. Larger batches also produce wider parquet partitions on disk; mixing widths is safe (ReplacingMergeTree dedupes ClickHouse-side and the dataset directory has no uniform-width invariant) but expect a one-time disk-space increase the first time a wider cycle re-writes the existing range |
| CLICKHOUSE_AUTO_IMPORT_MAX_ROWS_PER_FILE | Number | 1000000 | Split parquet partitions into multiple files once they exceed this row count |
| CLICKHOUSE_AUTO_IMPORT_IDLE_WAIT_TIMEOUT_S | Number | 600 | Max seconds to wait for the shared parquet-export singleton to go idle before starting a new export |
| CLICKHOUSE_AUTO_IMPORT_EXPORT_MAX_RETRIES | Number | 3 | Retry attempts when the parquet-export contends with another caller |
| CLICKHOUSE_AUTO_IMPORT_EXPORT_RETRY_DELAY_S | Number | 5 | Seconds to sleep between export retries |
| CLICKHOUSE_AUTO_IMPORT_DEBUG_RETRY_WINDOW_S | Number | 600 | Seconds the auto-import keeps retrying, with backoff from 5 s up to 60 s, when the gateway's /ar-io/admin/debug endpoint is unreachable or returns an error, as during a core restart. If a batch still cannot fetch it after this window, the cycle ends there and the next cycle picks up the remaining buckets. Retries are cut to fit the window (with a 10 s floor); 0 makes a single attempt. A value that is not a whole number stops the auto-import at startup |
| CLICKHOUSE_IMPORT_SKIP_UNCHANGED | Boolean | true | When true, clickhouse-import inserts only rows whose content (every column except inserted_at and expires_at) differs from the newest stored version of their key, so re-importing a height range no longer stacks duplicate ReplacingMergeTree versions. Real changes, such as an L1 offset backfilled after the first export, are still inserted and win. An unchanged re-import does not refresh expires_at. Set to false to insert every row, as before |
| PARQUET_EXPORT_DUCKDB_MEMORY_LIMIT | String | 2GB | Memory budget for the core-side parquet-export DuckDB instance. The export sorts each partition (COPY ... ORDER BY) and spills to disk past this limit instead of OOM-killing the worker. Set below the core container's memory ceiling. Lets a single export survive ultra-dense block heights (hundreds of thousands of data items / millions of tag rows) |
| PARQUET_EXPORT_DUCKDB_MAX_TEMP_DIRECTORY_SIZE | String | 64GB | Upper bound on DuckDB's on-disk spill for a single export. Guards against a runaway sort filling the data volume |
| PARQUET_EXPORT_DUCKDB_THREADS | Number | undefined | Optional cap on DuckDB worker threads during export. Fewer threads lowers peak sort memory. Unset → DuckDB default (one per core) |
When ClickHouse is configured alongside SQLite, GraphQL transaction queries hit both stores and merge the results. These settings let the composite backend skip SQLite for heights already covered by ClickHouse, avoiding duplicate scans. The buffer reserves recent heights (where ClickHouse ingestion may be partial) for SQLite.
The boundary only applies to SQLite's stable tables (stable_transactions,
stable_data_items). The new tables (new_transactions, new_data_items)
hold unstable and unconfirmed data — including pending rows with NULL
height that would be silently dropped by a height >= :minHeight predicate
— and are never covered by ClickHouse, so the floor is not applied to them.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| CLICKHOUSE_SQLITE_MIN_HEIGHT_ENABLED | Boolean | false | When true, restrict the SQLite fallback to heights above (ClickHouse max height - buffer) |
| CLICKHOUSE_SQLITE_MIN_HEIGHT_BUFFER | Number | 10 | Heights reserved for SQLite near the ClickHouse tip, to guard against partially ingested recent blocks |
| CLICKHOUSE_MAX_HEIGHT_CACHE_TTL_SECONDS | Number | 60 | TTL for the cached ClickHouse max-height lookup used by the boundary optimization |
| CLICKHOUSE_QUERY_TIMEOUT_SECONDS | Number | 3 | Timeout for ClickHouse queries, applied both as server-side max_execution_time and client-side HTTP request_timeout |
| CLICKHOUSE_GQL_MAX_ROWS_TO_READ | Number | 10000000 | Per-query max_rows_to_read hint applied to GraphQL queries against the transactions table. Queries that would scan more than this throw Code: 158, preventing accidental full-table scans if a skip index is bypassed |
| CLICKHOUSE_GQL_OWNER_PROJECTION_ROUTING_ENABLED | Boolean | false | When true, owner-filtered GraphQL transactions queries are routed through owner_projection. The main transactions table is height-ordered, so a sparse owner's rows scatter across millions of granules and finding a page can trip the row cap; the owner-ordered projection seeks straight to the owner's slice. No-id owner queries also gain a reactive height-windowing fallback if they still trip max_rows_to_read. Also routes owners + ids queries through the projection — a multi-id id IN (...) filter lights up most of id_bloom's granules (≈ a half-table scan, the dominant source of TOO_MANY_ROWS), so adding an owner and seeking the owner's slice keeps it under the cap; note owners + ids does NOT get the windowed retry (the walk is height-ordered and id queries carry no cursor predicate), so a whale owner + ids still surfaces the 158. Disabled by default for staged rollout — when off, owner queries plan exactly as before |
| CLICKHOUSE_GQL_OWNER_PROJECTION_ENTITY_TYPES | String | drive,folder,snapshot |
Comma-separated allowlist of Entity-Type tag values eligible for owner_projection routing of tag queries (only consulted when CLICKHOUSE_GQL_OWNER_PROJECTION_ROUTING_ENABLED is true, and not applied to owners + ids queries, which are bounded by the id list). A no-id query qualifies only when it carries an Entity-Type filter whose values are ALL in this list. file is deliberately omitted: an owner can have millions of files, and routing forces a full sort of the matched set per page (read-in-order is disabled to use the projection), re-done on every page — those large-result queries want the dedicated owner-ordered table instead. Bare-owner and owner+other-tag queries are likewise excluded |
| CLICKHOUSE_GQL_ID_LOOKUP_ENABLED | Boolean | false | When true, GraphQL transactions queries with ids or bundledIn first resolve those ids through the ClickHouse transaction_ids lookup table, then read transactions by primary key. Without it, an id lookup goes through id_bloom, which passes ~1% of all granules per id (~4M rows per id at ~400M rows, so 3 ids exceed the default CLICKHOUSE_GQL_MAX_ROWS_TO_READ), and bundledIn has no index. An id missing from transaction_ids is treated as absent from ClickHouse, so enable this only after the one-time backfill described in src/database/clickhouse/schema.sql (new rows are added automatically by transaction_ids_mv). A bundledIn query with a parent missing from the table, or a failed lookup, runs as before. See clickhouse_gql_id_lookup_total |
| GQL_L1_ONLY_ROUTING_FILTER | String | {"never": true} | Composable-filter DSL classifying which GraphQL transactions queries may be served exclusively from the L1 (base-layer) SQLite index, bypassing ClickHouse. When an incoming query is provably confined to L1 by this filter, it is answered by the stable_transactions tag-name+value PK seek — which sidesteps ClickHouse's TOO_MANY_ROWS row-scan cap — over the full block history, and ClickHouse is skipped entirely. Uses the same grammar as the indexing filters, but restricted to its monotone subset (tags, attributes, and, or, always, never); not / isNestedBundle / hashPartition are rejected at startup because entailment over non-monotone predicates could drop valid data-item results. The filter is an operator assertion that queries it entails are L1-only (e.g. {"tags":[{"name":"App-Name","value":"SmartWeaveAction"}]}) — any bundled data-item matches are intentionally excluded from routed queries, so only assert this for tags whose corpus is genuinely base-layer. {"always":true} routes every query to L1-only (drops all data-item results globally — a footgun). Default {"never": true} leaves routing off. Only consulted when a ClickHouse GraphQL backend is configured |
| CLICKHOUSE_GQL_DEDUPE_HEADROOM | Number | 4 | Multiplier applied to pageSize + 1 to size the inner LIMIT of the GQL transactions query (LIMIT 1 BY dedupe runs after it). When the inner window comes back full of duplicate ReplacingMergeTree versions and yields fewer than pageSize + 1 unique rows, the query continues from the last unique row's cursor (id lookups retry with 4x the headroom). A page still short after the last round is returned as a prefix with hasNextPage: true; see clickhouse_gql_dedupe_headroom_total |
| CLICKHOUSE_SQLITE_CIRCUIT_BREAKER_TIMEOUT_MS | Number | 5000 | Timeout (ms) for the SQLite leg of the composite GQL transactions query. On timeout the response degrades to ClickHouse-only results; ClickHouse timeouts are governed separately by CLICKHOUSE_QUERY_TIMEOUT_SECONDS and still propagate to the caller |
| CLICKHOUSE_SQLITE_CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE | Number | 80 | Error rate (0–100) within the rolling window that trips the SQLite-leg breaker open |
| CLICKHOUSE_SQLITE_CIRCUIT_BREAKER_ROLLING_COUNT_TIMEOUT_MS | Number | 60000 | Rolling window (ms) over which the SQLite-leg breaker's error rate is measured |
| CLICKHOUSE_SQLITE_CIRCUIT_BREAKER_RESET_TIMEOUT_MS | Number | 30000 | How long the SQLite-leg breaker stays open before transitioning to half-open for a trial request |
When enabled, indexed blocks, L1 transactions, and ANS-104 data items are
streamed into ClickHouse new_blocks / new_transactions so the unstable
head (~18 confirmations of recent data) is queryable from CH directly
instead of only from SQLite. The stable Parquet pipeline is unchanged —
once a row stabilizes it lands in transactions via parquet-export and
the unstable copy ages out via TTL. GraphQL merges the two transparently.
Streaming is opt-in. The default-off mode preserves today's behavior
exactly. Operators who want CH to be the sole read path can pair
CLICKHOUSE_STREAMING_ENABLED=true with CLICKHOUSE_GQL_SKIP_SQLITE_READS=true.
SQLite WRITES are unaffected by either flag — the indexer still feeds
SQLite for the Parquet export pipeline.
The unstable tables are created idempotently by scripts/clickhouse-import.
On streaming-only deployments the streamer fails closed at startup if the
tables are missing — operators run the import script once to bootstrap.
| ENV_NAME | TYPE | DEFAULT_VALUE | DESCRIPTION |
|---|---|---|---|
| CLICKHOUSE_STREAMING_ENABLED | Boolean | false | Master switch for the streaming pipeline. When true: indexer streams unstable rows into ClickHouse, GraphQL adds a third merge leg over new_transactions, and the SQLite leg drops to a tight-timeout fallback governed by CLICKHOUSE_SQLITE_FALLBACK_CIRCUIT_BREAKER_TIMEOUT_MS. When false (default): behavior is identical to the pre-streaming 2-leg path |
| CLICKHOUSE_NEW_TX_TTL_MINUTES | Number | 240 | TTL (minutes) for rows in new_blocks and new_transactions. Once a row stabilizes it lands in transactions via the Parquet pipeline; the unstable copy expires after this window. Default of 4h is comfortably past 18 confirmations × ~2 min/block plus a 1h auto-import cycle, so a stalled chain or delayed import won't expire rows before they stabilize. Read by scripts/clickhouse-import at schema-render time, not at runtime |
| CLICKHOUSE_STREAMER_BATCH_SIZE | Number | 500 | Maximum rows the streamer buffers before issuing a bulk INSERT. A time-based flush (CLICKHOUSE_STREAMER_FLUSH_INTERVAL_MS) ensures rows aren't held indefinitely when ingest is slow |
| CLICKHOUSE_STREAMER_FLUSH_INTERVAL_MS | Number | 1000 | Time-based flush interval (ms). Caps unstable-head latency at this value plus the bulk-insert RTT |
| CLICKHOUSE_STREAMER_QUEUE_MAX_SIZE | Number | 10000 | Hard cap on the streamer's in-memory buffer. Once exceeded, the streamer drops oldest rows to keep memory bounded under sustained CH unavailability — drops are logged and surface as a Prometheus metric. Dropped rows land via the stable Parquet pipeline once they stabilize |
| CLICKHOUSE_GQL_SKIP_SQLITE_READS | Boolean | false | When true, GraphQL queries skip the SQLite leg entirely. Pairs with CLICKHOUSE_STREAMING_ENABLED for operators who want CH to be the sole read path. Has no effect on SQLite WRITES |
| CLICKHOUSE_SQLITE_FALLBACK_CIRCUIT_BREAKER_TIMEOUT_MS | Number | 250 | Replacement for CLICKHOUSE_SQLITE_CIRCUIT_BREAKER_TIMEOUT_MS when streaming is enabled. In that mode CH-unstable covers the live tip and SQLite is a last-resort fallback for outages — a tight timeout keeps GraphQL responsive when CH itself is healthy. Distinct env var so operators running both modes on different nodes don't have to pick one envelope |