Self-Hosting & Configuration
On ANTS Cloud the gateway is operated for you. This page is for self-hosted deployments, where the gateway runs as its own service next to web and worker. It is a separate, internet-facing service and is off by default.
Requirements
- An Enterprise self-hosted license, which provides the
llm-gatewayentitlement. - The
gatewayAifeature flag enabled for the users who manage Budget AI, orLANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true. See What you need to turn it on. - The same Postgres and Redis as the rest of the platform, the same event storage bucket that ingestion uses, and a running
worker. The worker processes gateway traces and runs the budget and key maintenance jobs. - The gateway must share
SALTandENCRYPTION_KEYwithweb.SALThashes keys; changing it invalidates every issued key.ENCRYPTION_KEY(64 hex characters) opens provider secrets stored in the database.
Enable with Helm
The gateway ships as the gateway subchart of helm/ants-platform.
Turn on the service and set its public address
global.gateway.publicBaseUrl is the externally reachable address callers point their SDKs at, ending in /v1. It is rendered into web as GATEWAY_PUBLIC_BASE_URL and shown in Project Settings → Budget AI and in key snippets. When it is empty, the settings tab reports that the gateway address is not configured and hides the snippets.
Publish it through the ingress
The gateway ingress routes /v1 only, to the gateway service. Its host must match global.gateway.publicBaseUrl. Enabling the ingress without gateway.enabled: true fails the render.
Set the proxy hop count
Set gateway.config.trustedProxyHops to the number of proxies in front of the gateway. See Client addresses behind proxies.
Deploy and turn on serving per project
After the rollout, an administrator turns on Serve gateway traffic for this project in Project Settings → Budget AI for each project that should use it.
Helm values
| Value | Default | Purpose |
|---|---|---|
gateway.enabled | false | Deploy the gateway |
gateway.replicaCount | 2 | Replicas when autoscaling is off |
gateway.image.repository / tag | ants-gateway | Gateway image |
gateway.service.port | 3040 | API port, published by the Service |
gateway.service.adminPort | 9091 | Admin port for health and metrics; a container port only, never on the Service |
gateway.config.providerTimeoutMs | 60000 | Provider connect, headers and body timeout |
gateway.config.providerStreamUsage | "true" | Ask OpenAI-compatible and Azure OpenAI providers for a usage chunk on streams |
gateway.config.maxBodyBytes | 4718592 | Request body limit |
gateway.config.configPollMs | 1000 | Configuration version poll interval |
gateway.config.configStaleMs | 5000 | Fail-closed bound; must exceed configPollMs |
gateway.config.configFullReloadMs | 60000 | Full configuration reload interval |
gateway.config.probeIntervalMs | 60000 | Health check tick and default interval |
gateway.config.probeTimeoutMs | 10000 | Timeout per health check |
gateway.config.telemetryConcurrency | 16 | Concurrent trace recordings per replica |
gateway.config.trustedProxyHops | 1 | Proxies in front of the gateway |
gateway.config.trustedProxyCidrs | empty | Exact proxy addresses or CIDRs to trust instead of a hop count |
gateway.config.secretsTtlMs | 300000 | How long a secret read from AWS Secrets Manager is cached |
gateway.config.database.host / redis.host | empty | Hosts the init containers wait for |
gateway.resources | 200m / 256Mi requests, 1 CPU / 1Gi limits | Container resources |
gateway.autoscaling.enabled | false | Horizontal Pod Autoscaler, 2 to 6 replicas at 70 % CPU by default |
gateway.podDisruptionBudget.enabled | true | Keeps at least one replica during voluntary disruptions |
gateway.terminationGracePeriodSeconds | 120 | Lets streamed completions finish during a rollout |
gateway.serviceAccount.annotations | empty | For example an IAM role annotation when secrets are in AWS Secrets Manager |
gateway.networkPolicy.enabled | false | Egress policy, see Network policy |
global.gateway.publicBaseUrl | empty | Public address, see above |
global.gateway.secrets.store / region / prefix | db / empty / ants/gateway | Provider secret storage, read by both web and the gateway |
global.ingress.gateway.* | disabled | Gateway ingress host, TLS and annotations |
The gateway image is built from gateway/Dockerfile. A deployment that leaves gateway.enabled: false does not need it.
Environment variables
The gateway validates its configuration at start-up and refuses to start when it is incomplete.
| Variable | Default | Purpose |
|---|---|---|
PORT / HOSTNAME | 3040 / 0.0.0.0 | API listener |
GATEWAY_ADMIN_PORT | 9091 | Admin listener for /health, /ready and /metrics |
SALT | required | Key hashing; must match web |
ENCRYPTION_KEY | required, 64 hex characters | Opens database-stored provider secrets; must match web |
DATABASE_URL, Redis and event storage settings | as for worker | Configuration loading, admission and trace recording |
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION, LANGFUSE_EE_LICENSE_KEY | unset | Plan resolution for the llm-gateway entitlement |
GATEWAY_PROVIDER_TIMEOUT_MS | 60000 | Provider timeout |
GATEWAY_PROVIDER_STREAM_USAGE | true | Request usage chunks on OpenAI-compatible and Azure OpenAI streams |
GATEWAY_MAX_BODY_BYTES | 4718592 | Request body limit |
GATEWAY_TRUSTED_PROXY_HOPS | 1 | Proxies in front of the gateway |
GATEWAY_TRUSTED_PROXY_CIDRS | unset | Proxy addresses or CIDRs to trust; takes precedence over the hop count |
GATEWAY_CONFIG_POLL_MS | 1000 | Version poll interval |
GATEWAY_CONFIG_STALE_MS | 5000 | Fail-closed bound; must be longer than the poll interval |
GATEWAY_CONFIG_FULL_RELOAD_MS | 60000 | Full reload interval |
GATEWAY_SECRETS_STORE | db | db or aws; must match web |
GATEWAY_SECRETS_REGION | unset | AWS region for Secrets Manager |
GATEWAY_SECRETS_PREFIX | ants/gateway | Secret path prefix; must match web |
GATEWAY_SECRETS_TTL_MS | 300000 | Cache lifetime for managed secrets |
GATEWAY_PROBE_INTERVAL_MS | 60000 | Health check tick and default interval |
GATEWAY_PROBE_TIMEOUT_MS | 10000 | Timeout per health check |
GATEWAY_PROBE_CONCURRENCY | 8 | Health checks in flight per replica, up to 64 |
GATEWAY_TELEMETRY_CONCURRENCY | 16 | Concurrent trace recordings |
GATEWAY_TELEMETRY_MAX_QUEUE | 10000 | Trace recordings that may wait |
BUILD_ID | unset | Reported on traces as gateway.version |
OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_SERVICE_NAME | http://localhost:4318 / gateway | Operational tracing of the gateway itself |
On web, set GATEWAY_PUBLIC_BASE_URL (through global.gateway.publicBaseUrl) and the same GATEWAY_SECRETS_* values.
The gateway accepts only ANTS-issued keys and provider credentials registered in a project. It refuses to start while any of GATEWAY_DEV_KEY, GATEWAY_DEV_PROJECT_ID, GATEWAY_PROVIDER_BASE_URL or GATEWAY_PROVIDER_API_KEY is set.
Provider secret storage
By default, provider secrets are encrypted into the platform database. To store new credentials in AWS Secrets Manager instead:
Both web (which writes secrets) and the gateway (which reads them) must see the same store, region and prefix. Scope the IAM policy to the prefix and grant GetSecretValue, PutSecretValue, CreateSecret, DeleteSecret and RestoreSecret; without the last two, deleting a credential or moving it back out of the managed store leaves the secret behind. Existing database credentials are moved one at a time with Move in Models & Routing. See Where provider secrets are stored.
Client addresses behind proxies
Per-key allowed source networks are checked against the caller's address, which the gateway reads from X-Forwarded-For counting proxies from the right. Set trustedProxyHops to exactly the number of proxies in your path: for example 1 for a load balancer straight to the service, 2 for a load balancer then an ingress controller, 0 when connections reach the pod directly.
- Too high, and a caller can forge its own address and bypass network allow-lists.
- Too low, and the gateway sees a proxy's address, so every network-confined key is refused.
trustedProxyCidrs lists the proxies exactly instead and takes precedence when set.
Ports, health and metrics
The gateway listens twice:
| Port | Serves | Exposure |
|---|---|---|
API (3040) | /v1/*, /health, /ready | Published by the Service and the gateway ingress (/v1 only) |
Admin (9091) | /health, /ready, /metrics | Container port only. Do not publish it. |
/metrics reports per-project and per-key counts, so it is kept off the public port. It returns JSON with request counts by status, refusal and error counters by code, in-flight requests, gateway latency before the provider call (percentiles), attempt, retry, fallback and benching counts, budget exhaustion counts, health check results, telemetry recorded and failed counts, configuration statistics (projects, keys, last successful version read, pending projects) and process memory and event loop delay.
The chart's liveness probe uses /health and the readiness probe uses /ready, both on the admin port. /ready fails while the replica is draining or its configuration is stale, so the load balancer stops sending it traffic.
On SIGTERM the gateway marks itself not ready, lets in-flight requests and streams finish, and waits for pending trace recordings before exiting, within a shutdown deadline.
Configuration propagation and fail-closed behaviour
The request path never reads Postgres. Each replica holds an in-memory snapshot of every served project's keys, model groups, deployments, credentials and budgets.
- Every change in the product writes Postgres and its audit log entry in one transaction, then bumps the project's configuration version in Redis.
- Each replica polls the versions every second (
GATEWAY_CONFIG_POLL_MS) and reloads changed projects. A blocked key, a removed grant or a disabled project stops working on every replica within about a second. - Every 60 seconds (
GATEWAY_CONFIG_FULL_RELOAD_MS) each replica reloads every enabled project. This picks up entitlement changes and any version bump that failed; the product says "Saved, but not yet in force" in that case. - Fail closed: if no version read has succeeded within 5 seconds (
GATEWAY_CONFIG_STALE_MS), or a project's pending change has not loaded within that time, requests for that project get 503gateway_config_stale. When the version read itself is failing, every request is refused and/readyfails.
Dependencies
| Dependency | Used for | If it is unavailable |
|---|---|---|
| Redis | Configuration versions, rate limit and budget counters, reservations, benching, deployment status, response cache, health check scheduling | Requests are refused with 503 gateway_config_stale; nothing reaches a provider |
| Postgres | Loading project snapshots, recording threshold crossings and health check results | Replicas keep serving from their last snapshot until it is older than the stale bound for a changed project |
| Event storage and worker | Turning gateway records into traces, and the budget flush and key maintenance jobs | Requests are still served; traces and durable spend catch up when they recover |
| AWS Secrets Manager (optional) | Provider secrets in the managed store | Replicas keep the secrets they already hold |
The gateway does not connect to ClickHouse.
The worker runs two Budget AI jobs: one every 10 seconds writes budget and org unit spend from Redis to Postgres, and one every minute blocks keys whose rotation grace window has ended.
Network policy
With gateway.networkPolicy.enabled: true, the gateway's egress is limited to DNS, the platform data namespace and the CIDRs in gateway.networkPolicy.allowedEgressCidrs on the ports in allowedEgressPorts (default 443). A compromised gateway holds provider secrets in memory, so this limits what it can reach. Before enabling it, list every provider endpoint, and when secrets are in AWS Secrets Manager the Secrets Manager and STS endpoints. A Postgres or Redis hosted outside the cluster must also be listed, with its port.
Scaling
Replicas are stateless; all shared state is in Redis. Scale with replicaCount or the autoscaler. Rate limits, budgets, reservations and benching are shared across replicas. The Lowest latency and Least busy strategies use measurements taken on each replica. Keep terminationGracePeriodSeconds long enough for your longest streamed completions.