Skip to main content
Settings of the collect gateway (cmd/gateway, internal/gateway). Every value is a required environment key: the gateway refuses to start when one is missing, so no limit runs at a value nobody chose. Change a value only with a recorded reason in the table below.

Starting values

Other settings: GATEWAY_NODE_ID (unique per instance), GATEWAY_ADDR (for caddy), GATEWAY_OPS_ADDR (/healthz, /readyz, /metrics, private), DBIP_MMDB and TRACKER_RELEASE_DIR (both baked into the image), and the NATS_* keys. Fixed in code: publish acknowledgement wait 2 s (PublishAckWait), readiness canary every 1 s with a maximum age of 7 s (CanaryMaxAge, so /readyz fails within 10 s of a queue outage), 200,000 entries each for the replay table, the ip-cluster sets, the visitor volume table and each rate limit table. Each limit is per node. With three edge nodes, a site can send three times the site rate, and a captured proof can be replayed three times the replay limit. Acceptance criterion, met on 2026-11-12: no event of the phase 3 and phase 4 browser suites was refused, rate limited or quarantined against the real gateway (TestBrowserMatrix in tests/gateway).

Responses

202 queued (only after the JetStream acknowledgement), 204 dropped for Sec-GPC: 1, 400 malformed (also a missing User-Agent), 403 origin, challenge, proof, replay or fetch metadata, 404 unknown or inactive site, 410 revoked tracker version (the file, and an event whose sv names a revoked release), 413 body too large, 429 rate limited (events and challenges), 503 queue unavailable, registry not loaded or a missing client address header (events and challenges). Every response to a request with Origin carries Access-Control-Allow-Origin with that origin and Vary: Origin. The tracker files carry Access-Control-Allow-Origin: *, Cross-Origin-Resource-Policy: cross-origin and Cache-Control: public, max-age=31536000, immutable; there is no latest alias.

Readiness

/readyz checks, in this order: a JetStream acknowledgement within 7 s, that edge_secrets is not known to be missing, the registry snapshot, today’s proof key, today’s daily secrets. The canary is a create of a short lived key (canary.<node>.<ns>, ttl 30 s) in edge_secrets, not a message on WEB_EVENTS: a work-queue message would reach the replayer as an event. An outage of WEB_EVENTS alone therefore does not show in /readyz; event publishes then answer 503 directly. A restart of a single NATS server loses the memory buckets (edge_secrets, visit_state, locks) entirely, not just their keys; a full restart of the 3-node cluster keeps their definitions but empties them. Each service recreates the buckets it uses itself, with exactly the topology config from internal/natsx: the gateway edge_secrets, the replayer visit_state and locks, jobs and the metricswriter locks. They check at startup, every second, and at once when a bucket operation fails as a missing stream would. Their NATS users may create exactly those KV_<bucket> streams and nothing else. No operator step is needed (tests/recovery, build tag recoveryit). An emptied edge_secrets is the accepted trade-off: the day’s secrets and proof key are new, so visitors seen earlier that day are counted again and challenges issued before the restart stop verifying once the 10 s key refresh ran. Every gateway compares its loaded daily secrets with the bucket every 10 s and switches to the stored ones, so a gateway that kept running agrees with one started after the restart. Secrets are never persisted. Measured 2026-11-12 (tests/recovery), times after NATS was healthy again:

Throughput

Measured 2026-11-12 on the Office host (Intel Core Ultra 5 225, 10 cores), one gateway, one NATS server with file storage, all in Docker on one machine. Every request is a full POST /v1/e: parse, proof, origin, rate limits, visitor token, country, ip-cluster, sanitization and a JetStream publish waited for. Traffic is spread over 20 sites and random 10.0.0.0/8 client addresses so the limiter does not cap it. Target: at least 2,000 req/s with p99 under 50 ms on one instance. Met by both. make gateway-load is a manual target and not part of make gateway-it or the nightly e2e workflow. On GitHub’s standard runner for private repositories (2 vCPUs, 7 GB) k6, the gateway and NATS share two cores; two runs there sent 2,400 and 2,221 req/s with p99 174 ms and 222 ms (e2e runs 43437576423 and 43454017432), which measures the runner, not the gateway. Run it on a host with cores to spare for k6 before a release that touches the event path. The production edge VM (Standard_B2als_v2, 2 vCPUs) has not been measured with this test. Under saturation (160 concurrent clients) the in-process node reached 7,721 req/s with p99 79.8 ms, so latency at the target rate comes from queueing, not the path.

Dropped stats

The jobs edgestats task needs a site’s tenant from the site registry. If an EDGE_STATS counter names a site that is still not in the registry after every retry (about 36 seconds), that counter is dropped and logged at error level. The jobs_edgestats_dropped_total metric counts the dropped site counts and reports 0 from startup. jobs_edgestats_unattributed_total{outcome} breaks the drops down by outcome. The other counters in the message are still stored.