Configuration reference

All configuration is environment variables — no config file. Unknown ANTARES_STORE/ANTARES_BUS values are fatal at startup, never silently defaulted. This table is checked against the source by dev/check-env-docs.sh (CI fails when a variable exists in code but not here).

Core

VariableDefaultEffect
ANTARES_STOREmemoryStore mode: memory, file, postgres, timescale. Unknown value = fatal.
ANTARES_TEMPORALfollows ANTARES_STORETemporal driver: a store mode, or none — history off. Mix freely with ANTARES_STORE, e.g. file current state with timescale history; temporal reads answer OperationNotSupported (422, CIM 009 Table 6.3.2-1) and nothing is recorded. A backend different from the store builds a second store instance used only for history.
ANTARES_HTTP_PORT9090HTTP listen port.
ANTARES_METRICS_PORTunsetA second listener that serves GET /q/metrics and answers every other path 404; with it set, the API port answers /q/metrics 404, so a NetworkPolicy can admit a scraper to this port without the API. Unset, /q/metrics stays on the API port. Port 0, the API port or a non-number is fatal at startup. ANTARES_ADMIN_METRICS applies on this port as on the API port.
ANTARES_ROLESallComma list of roles this process runs: api, matcher, notifier, temporal, registry — the role-split fleet shape.
ANTARES_BUSlocalChange-event bus: local (in-process, single node) or nats (JetStream, multi-pod). Unknown value = fatal.
ANTARES_HOST_ALIASantaresThis broker's name in federation Via chains (CIM 009 6.3.18) — loop detection identity. Two LB'd replicas of one logical broker share one alias.
ANTARES_PUBLIC_URLhttp://{host_alias}:{port}The URL peers can reach this broker at: forwarded subscription copies notify {ANTARES_PUBLIC_URL}/ex/v1/remote-notify (5.8.1.4). Set it whenever the default is not routable from peers.

Store backends

VariableDefaultEffect
ANTARES_DATA_DIR— (required for file)Directory for the redb file. Must be a mounted volume — data never lives inside the image.
ANTARES_DATABASE_URL— (required for postgres/timescale)PostgreSQL connection string; PostGIS required, TimescaleDB for timescale. Bounded startup retry while the DB boots.
ANTARES_REQUIRE_RLSunset1/true: refuse to start when the DB role bypasses Row-Level Security (defense-in-depth for shared-schema multi-tenancy).
ANTARES_PG_POOL20Connection-pool size for postgres/timescale. Unparsable value = fatal. Sessions carry lock_timeout 5 s. A request that waits the pool's 5 s acquire timeout without getting a connection is answered 503 with Retry-After — see the sizing formula in the operations runbook.
ANTARES_PG_WRITE_RESERVE0How many of the ANTARES_PG_POOL connections only batch writes (create, upsert, update) take: the reads share the other ANTARES_PG_POOL - reserve, so a flood of reads queued on the pool cannot hold a pipeline's batch upsert behind them. 0 is off (one shared pool). Must be below ANTARES_PG_POOL; anything else is fatal. Size it to the batches that run at once (ANTARES_BATCH_INFLIGHT, 8): a batch that finds the reserve full waits for it, up to the 5 s acquire timeout. Published as storeInfo.poolWriteReserve in /q/health.
ANTARES_PG_STATEMENT_TIMEOUT_MS30000Per-session statement_timeout on every pooled connection: a query past it is cancelled and answered InternalError (500, "database statement timeout"; CIM 009 5.5.2 names database timeouts as InternalError). Migrations are exempt. Not a positive integer = fatal.
ANTARES_PG_BATCH_ASYNC_COMMITfalsetrue: the batch writes (entityOperations create, upsert, update and merge, plus the bulk attribute purge that runs through the same store calls) commit with synchronous_commit = off, set LOCAL to their own transaction; every other write stays synchronous. A Postgres crash then loses at most the last ~3 x wal_writer_delay (600 ms by default) of acknowledged batches, never their atomicity; notifications already sent for them stay sent. For a feed that rewrites its Entities every second (live vehicle positions), where the next batch restores what a crash lost. Not a boolean = fatal.
ANTARES_SECRET_KEYunset32 bytes as 64 hex digits (openssl rand -hex 32) or base64, from a Secret. With it, every Subscription, Context Source Registration Subscription and Snapshot receiverInfo value, every endpoint URI that carries user:password@ userinfo, and the resolved credentials of a dead letter are stored sealed (AES-256-GCM, enc:v1: prefix) in the postgres/timescale store and opened on read; at boot the broker seals the rows written before the key was set. With ANTARES_BUS=nats the subscription KV mirror carries them sealed the same way, so every api and matcher pod needs the same key: a matcher pod that cannot open an entry drops that subscription from its mirror (logged) rather than notify with ciphertext. Without it the values are stored in plaintext and the boot logs a warning. A value that is not 32 bytes = fatal. A sealed value read without the key, or under another key, fails that read with a 500, so keep the key for as long as the database holds rows sealed under it. The memory and file stores ignore it.
ANTARES_MIGRATEon0/false skips running migrations from this process, so serving replicas do not race the DDL — run them once from a job or init container instead.
ANTARES_ALLOW_SHARED_LOCALunset1 permits bus=local with a postgres/timescale store — safe ONLY for a strictly single-process deployment; two such processes double-fire notifications.
ANTARES_TEMPORAL_RECORDallHistory gate for the entity endpoints. all: every changed attribute instance is recorded. observed: only instances carrying observedAt enter history — a metadata-only write (no observedAt) updates current state and its modifiedAt but leaves no history, so timeproperty=modifiedAt/createdAt temporal queries return nothing for never-observed attributes. none: the entity endpoints record nothing; the temporal API still stores and serves what it is given directly (unlike ANTARES_TEMPORAL=none, which switches the temporal seam off). ruled: history is opt-in, as all for an Entity whose own historyRetention, or a stored retention rule for its type or Tenant, gives a duration or forever (recording observed unless its rule names another record), nothing for any other. The ETSI temporal suites assume all, which is why it stays the default. Unknown value = fatal.
ANTARES_NON_OBJECT_BODYinvalid-requestBehaviour profile (pinned by crates/antares-api/tests/behaviour_profiles.rs): the error a JSON body that is no object ([], [1,2,3]) gets where an Entity, fragment, Subscription or registration is expected. invalid-request as ETSI TP 001_02_02 asserts; bad-request-data per TS 104 175 8.2.3 (valid JSON, no valid datatype), as forge all-fixes and Coraine assert. A body that is not JSON stays InvalidRequest. Unknown value = fatal.
ANTARES_FED_WRITE_SOURCE_404reportBehaviour profile: on a distributed write, a part answered 404 beside a successful part. report keeps the 404 in a 207 (ETSI D002_02_01_inc, D002_02_02_inc); ignore reads it as "no data" (TS 104 176 6.3.5) and answers the success (forge all-fixes, Coraine). A 404 on every part stays one 404, any other failure stays in the 207. Unknown value = fatal.
ANTARES_IMPLICIT_TENANTSwith-upsertBehaviour profile: which writes create an unknown Tenant (TS 104 175 7.8.2). with-upsert: the operations 7.8.2 lists plus Batch Upsert; listed: only the 7.8.2 list, so Batch Upsert on an unknown Tenant answers NonexistentTenant (Coraine tenant_implicit_create). Unknown value = fatal.
ANTARES_TEMPORAL_RETENTION_DAYSunset (keep forever)Temporal history retention; the sweep job prunes older attribute instances. Applies to the temporal half wherever it lives: a file store with postgres history still runs the job.
ANTARES_MEM_MAX_INSTANCES10000Most instances one Attribute's history keeps in the memory/file store, which holds an Entity's whole history as one document (cloned, and in file mode rewritten and fsynced, on every append, under the store's single write lock). A temporal API write (5.6.11, 5.6.12) that would grow an Attribute past it is refused with BadRequestData (400) and writes nothing; an Entity write still commits, but its history is not recorded for that Entity (counted in /q/health temporalDrainErrors, logged). Corrections and deletions of instances are never refused. No retention job prunes this history (ANTARES_TEMPORAL_RETENTION_DAYS runs on Postgres history), so the broker logs one warning at start when history lives here. 0 = no cap. Not a non-negative integer = fatal. The Postgres stores ignore it.
ANTARES_TEMPORAL_DEDUPonoff records a history row for every admitted write; on, the Postgres stores record a new observation only (Storage, "What enters history").
ANTARES_TEMPORAL_INSTANCE_LIMIT1000Most instances of one Attribute a temporal response serves before it answers 206 with a Content-Range (6.3.10): a week of a 10-minute sensor. The ETSI suite's brokers set 9, since its 206 cases create 20 instances.
ANTARES_MAX_LIMIT1000Most items one page answers (limit; past it 403 TooManyResults): every paged list, Entities (GET /entities, POST /entityOperations/query), Temporal Evolutions (GET /temporal/entities, POST /temporal/entityOperations/query), Subscriptions, Context Source Registrations and their Subscriptions, and the admin dead letters.
ANTARES_DEFAULT_LIMIT100Items a page of the same lists answers when the request names no limit; a Link: rel="next" names the next page. Never above ANTARES_MAX_LIMIT.
ANTARES_TEMPORAL_DEFAULT_INSTANCES100Instances of one Attribute a temporal response serves when the request names no lastN/firstN; past it the answer is 206 with a Content-Range, as at the ceiling. Never above ANTARES_TEMPORAL_INSTANCE_LIMIT (the ETSI brokers at 9 stay at 9).
ANTARES_SWEEP_SECS900Cadence of the background GC sweep (expired entities/registrations, 4.22) — identical across store modes.

NATS scale-out

VariableDefaultEffect
ANTARES_NATS_URL— (required for bus=nats)NATS server URL; JetStream streams and the subscription KV bucket are asserted at startup.
ANTARES_NATS_REPLICAS1JetStream replica count for streams/KV (set 3 on a 3-node NATS cluster).
ANTARES_OUTBOX_DRAINonoff disables the notification outbox drainer in this process (crash-drill lever / dedicated-drainer split).

Federation & egress hardening

VariableDefaultEffect
ANTARES_EGRESS_ALLOW_PRIVATEtrueBroker-initiated HTTP and MQTT (notifications, forwards, @context fetches) may reach loopback, link-local and RFC 1918 destinations. Set false (or 0) on an internet-exposed deployment to deny those together with carrier-grade NAT (100.64.0.0/10), 0.0.0.0/8, the IETF assignment and benchmarking blocks and the reserved space above 240.0.0.0. The cloud instance-metadata endpoints are refused whatever this is set to, in every IPv6 spelling. The scheme allowlist, redirect cap, DNS pinning and response-size caps apply regardless. A refused delivery is booked as a failure (lastFailure, status: failed) and never retried.
ANTARES_EGRESS_PRIVATE_HOSTSunsetComma-separated host names that broker-initiated HTTP and MQTT may still reach at a private address while ANTARES_EGRESS_ALLOW_PRIVATE=false: the in-cluster peers, such as a gateway Service or a federated context source. Names compare case-insensitively without a trailing dot; empty members and address literals are ignored (an address is judged by its range). A listed name never reaches loopback, 0.0.0.0/8 or instance metadata. Every unlisted private destination stays refused. Read once at start.
ANTARES_CONTEXT_RESOLVE_TIMEOUT_MS30000Wall-clock bound on resolving one request's @context: every remote document it references (up to 32, each fetch bounded at 10 s on its own), the wait for a fetch slot (32 cold fetches in flight across the process) and the cache write-through. Past it the request is answered LdContextNotAvailable (504, CIM 009 5.5.6). Not a positive integer = the default.
ANTARES_FED_FANOUT8Concurrent forwards per distributed read (4.3.6.1 orders the merge, not the requests).
ANTARES_FED_INFLIGHT256Forwarded requests in flight for the whole process; callers over the cap wait. Bounds the buffers and connections open federated queries hold (6 000 open queries × 34 sources once reached 7.7 GB).
ANTARES_MAX_FED_RESPONSE_BYTES16777216 (16 MiB)Ceiling on one forwarded response body — one misbehaving peer cannot balloon broker memory. Over-cap parts fail as warning 111 (Table 6.3.17-1).
ANTARES_MAX_BATCH_ITEMS1000Batch entity-count cap (DoS bound; the spec sets none). Raise for trusted bulk producers.
ANTARES_MAX_BODY_BYTES4194304 (4 MiB)Request body cap, answered with a bare 413 (6.3.4). One number governs the extractor limit and the bounds wall.
ANTARES_POLICYallow-allThe policy engine every operation is asked about (ADR-0020). The shipped binary is built with allow-all alone, which decides nothing; an unknown name is fatal at startup and names the shelf the binary was built with, so a typo cannot quietly serve every request wide open. An engine that refuses is answered 403 with the ProblemDetails type urn:antares:error:AccessDenied and the engine's own reason — this broker's own URN, because Table 6.3.2-1 names no access-denied error and none is invented under the ETSI namespace. An engine that narrows instead conjoins its condition into the query the store runs and drops the members it named from every document served; a read narrowed that way answers Antares-Results-Restricted: true when the engine asked for the marker, and is otherwise silent. A single Entity outside the narrowing answers 404 like an absent one, so a caller cannot tell the two apart. The engine is also asked about each notification before it is sent: it may drop one — which is no delivery attempt, so timesSent and lastNotification do not move — or project the entities it carries.
ANTARES_POLICY_SUBJECT_HEADERSunset (no headers)Comma list of request headers copied into the subject the engine is given, matched case-insensitively. The broker never interprets them and never lets them leave: they are stripped from forwarded requests and absent from notifications, dead letters and logs. One copy is persisted: a Subscription, an EntityMap and a Snapshot each keep their creator's headers, because all three are used again after the request that made them — 5.8.6 delivery is broker-initiated, a snapshot's fill runs once its request has been answered, and an EntityMap presented by a different subject is treated as one that cannot be accessed (5.5.14), so a new one is built for that request. That copy is a broker-internal member — no representation renders it, no client can set it, and the 5.8.1.4 copy forwarded to a Context Source is stripped of it — but it does live in the store, so name a header that identifies the subscriber, not one that authenticates them. An embedding binary that sets subject_required on the state makes a request that carries none of them a control-plane call: the NGSI-LD API serves it only with the ANTARES_ADMIN_TOKEN_FILE bearer and answers 401 otherwise, or always when no admin token is configured. Under subject_required the headers count only on an in-process call: the broker reads no gateway mesh identity, so a request from outside the process is treated as carrying none of them and needs the admin bearer too.
ANTARES_POLICY_TIMEOUT_MS250How long the policy engine has to answer one request before the seam stops waiting and denies (ADR-0020). The engine the broker ships allows everything and never waits, so this matters only to a deployment that attached its own.
ANTARES_CORS_ORIGINSunset (no CORS headers)Browser origins allowed, comma-separated, or *. Preflights are answered for every method and header; Link, NGSILD-Tenant and NGSILD-Results-Count are exposed.
ANTARES_API_SURFACESadminComma list of HTTP surfaces mounted beside the NGSI-LD API root, each under its own reserved prefix (admin serves /q). An unknown name is fatal at startup and names the shelf the binary was built with; a selection that leaves out admin serves no /q at all, probes included.
ANTARES_ADMIN_TOKEN_FILEunset (no credential)A file holding the bearer secret every /q route except health and readiness then requires (Authorization: Bearer <secret>, at least 32 bytes, one trailing line break ignored; Admin API). Read once at start: an unreadable file or a shorter secret is fatal, so a configured credential never silently becomes none. Unset, /q asks for nothing and the broker logs one warning at start.
ANTARES_ADMIN_METRICStokenopen serves /q/metrics without the admin secret, for a scraper that cannot send one; token keeps it behind the secret like the rest of /q. Any other value is fatal at startup. Without ANTARES_ADMIN_TOKEN_FILE it changes nothing.
ANTARES_HISTORY_BACKFILLunset (off)on makes a value older than the current one a backfill into history only, leaving the current state as it is. Off keeps NGSI-LD's last-write-wins, which the ETSI suite checks.
ANTARES_EXTRA_CA_FILEunsetPEM bundle of ADDITIONAL trust anchors for egress TLS (private CAs). Egress TLS trusts the host's certificate store (/etc/ssl/certs on Linux; the shipped image carries one), and this widens it. Verification itself is never disableable.

Notification delivery

VariableDefaultEffect
ANTARES_NOTIFY_ATTEMPTS1Delivery attempts per notification, first one included. 1 is 5.8.6 as written: one send, the outcome booked. Higher values retry on their own task with exponential backoff; the retries never move timesSent again.
ANTARES_NOTIFY_BACKOFF_MS1000Delay before the first retry; doubles per retry (±20 % jitter, 60 s ceiling).
ANTARES_NOTIFY_MAX_AGE_SECS300No retry starts later than this after the first attempt. When the attempts or the age run out the notification becomes a dead letter (/q/dead-letters, see operations).
ANTARES_DELIVERY_WIDTH64Notifications in flight at once across the whole broker. A slot is held until the endpoint answers, so the number that fits depends on the subscribers: local sinks free a slot in milliseconds, a remote endpoint sitting at its 30 s timeout (Table 5.2.15-1) holds one for the whole timeout. Published as deliveryWidth in /q/health.
ANTARES_DELIVERY_WIDTH_PER_TENANT8Of that width, what one tenant may hold, capped at the width. Without it a single tenant with enough dead endpoints holds every slot and nothing leaves the broker for anyone else. Published as deliveryWidthPerTenant.
ANTARES_BATCH_INFLIGHT8Batch writes running at once, whatever their size. Each holds a database connection while it runs, so keep it under ANTARES_PG_POOL: the rest of the pool stays with the reads, and a stream of small upserts cannot make a visitor wait for a connection. A batch waits for a slot inside the same 20 s as for its byte share, then gets 503 with Retry-After. Published as batchInflight in /q/health.
ANTARES_BATCH_INFLIGHT_BYTES4194304 (4 MiB)Batch write bodies (create, upsert, update, merge) in flight at once, in bytes. A batch costs about a hundred times its body while it runs, so a 1000-entity upsert of 3 MB holds about 300 MB. A batch that does not fit waits up to 20 s for running ones to finish, then gets a bare 503 with Retry-After, counted in antares_batch_overloaded_total. One batch always runs alone, however large. Published as batchInflightBytes in /q/health.
ANTARES_DELIVERY_LANE_BYTES134217728 (128 MiB)Estimated heap all subscription delivery lanes may hold at once, across every tenant. A change group that would push the total past it is not queued: that subscription's lane counts a drop (antares_notification_lane_dropped_total), as a full lane does. One group is always admitted into empty lanes. The held bytes are the gauge antares_notification_lane_bytes; published as deliveryLaneBytes in /q/health.

Lifecycle & observability

VariableDefaultEffect
ANTARES_HEADER_READ_TIMEOUT_MS10000A connection that has not finished its request HEAD within this window is closed (slow-loris bound).
ANTARES_MAX_CONNECTIONS10000Concurrent-connection ceiling; further accepts are dropped. Counts keep-alive and LB health-check connections too — size accordingly.
ANTARES_DISCOVERY_SCAN_MAX100000Documents one unpaginated whole-tenant fold may read or hold: the /types//attributes discovery folds (5.7.5-5.7.10 define no pagination) the registration query (5.10.2.4 filters before it pages, so the whole match set is held) and the candidate walk of a paged geoquery or scopeQ. Past it the answer is 403 TooManyResults (5.5.6) instead of an unbounded scan. Published as maxFoldDocs in /q/health.
ANTARES_DRAIN_DELAY_MS2000Rolling update, step 2: keep serving this long after /q/health flips to 503 — the load balancer's notice window, sized so a health poll actually observes the 503 before the socket goes.
ANTARES_DRAIN_DEADLINE_SECS20Bound on waiting for in-flight connections during drain. Container stop_grace_period / terminationGracePeriodSeconds MUST exceed delay + deadline.
ANTARES_TELEMETRYoffAny value but an off spelling enables the metrics recorder and, with the endpoint, the OTLP span and log pipelines.
ANTARES_OTLP_ENDPOINTunsetOTLP/HTTP collector for traces and logs, e.g. http://collector:4318/v1/traces; log records go to the v1/logs twin of that URL with the same resource attributes. Unset costs nothing.

Compile-time bounds (no variable sets them; spec-shaped rejections): URI 8 KiB → 414, JSON depth 64 → 400. The body cap is ANTARES_MAX_BODY_BYTES above. Every bound in force is reported live by GET /q/health under limits.

Node-shim (wasm tier) extras: ANTARES_FILE (redb path per shim) — see the browser guide.