Skip to main content
The gateway is a sub-chart of the langwatch Helm chart. The umbrella chart enables it by default with gateway.chartManaged: true and runs it as its own Deployment, Service and Ingress next to langwatch-app. This guide takes you from an empty namespace to a gateway that answers a chat completion.

Prerequisites

  • A Kubernetes cluster and helm 3.
  • An ingress controller. The chart defaults to ingress.className: nginx.
  • A hostname for the gateway, separate from the app hostname. See DNS and TLS.
  • The LangWatch umbrella chart. Follow Kubernetes (Helm) for the app side first if you have not installed it yet.
1

Create the app Secret

The gateway pod and the app pod mount the same Secret. The gateway reads two keys from it: LW_GATEWAY_INTERNAL_SECRET signs every call between the gateway and the control plane, and LW_GATEWAY_JWT_SECRET verifies the JWTs the control plane issues for resolved virtual keys.With autogen.enabled=true the chart creates the Secret on first install. Otherwise create it before helm install:
gateway.secrets.existingSecretName (default langwatch-app-secrets) must equal the app Secret name. If you set secrets.existingSecret or autogen.secretNames.app to another name, set gateway.secrets.existingSecretName to the same name. The chart refuses to render when the two differ.virtualKeyPepper is read by the app only. The gateway never mounts it.
2

Write the gateway values

Add a gateway block to your umbrella values.yaml:
The image is docker.io/langwatch/ai-gateway. image.tag defaults to the chart appVersion. Set image.repository to your mirror if the cluster cannot pull from Docker Hub.
3

Install or upgrade

The umbrella chart bundles the gateway sub-chart, so one helm upgrade rolls the app and the gateway together. The gateway Deployment uses a RollingUpdate with maxSurge: 1 and maxUnavailable: 0.
4

Verify

Expected output:
Then run the post-install checklist to send a first completion through the gateway.

What the chart renders into the pod

The chart passes these environment variables to the container. Every other key in the gateway values.yaml (cache.*, bifrost.*, guardrails.*, budget.*, startup.*, otel.auth*, otel.batchTimeout, otel.maxQueueSize, security.*Timeout, controlPlane.*Timeout) does not reach the pod. The chart renders no authentication header for the OTLP export. Point otel.endpoint at a collector that accepts unauthenticated OTLP/HTTP, or leave it empty. The full variable reference is on Config. Write maxRequestBodyBytes as an integer literal. The chart renders it with %d, and a YAML float such as 3.3554432e+07 fails the render.

Rotate the JWT secret without downtime

The gateway verifies a JWT against LW_GATEWAY_JWT_SECRET first, then against LW_GATEWAY_JWT_SECRET_PREVIOUS when that variable is set. The control plane signs with the LW_GATEWAY_JWT_SECRET key of the same Secret, and a JWT lives 15 minutes.
  1. Edit the Secret: copy the current LW_GATEWAY_JWT_SECRET value into a new key LW_GATEWAY_JWT_SECRET_PREVIOUS, then replace LW_GATEWAY_JWT_SECRET with the new value.
  2. Set gateway.secrets.jwtSecretPreviousKey: LW_GATEWAY_JWT_SECRET_PREVIOUS and run helm upgrade. The gateway pods restart with both values. Wait for kubectl -n langwatch rollout status deploy/langwatch-gateway.
  3. Run kubectl -n langwatch rollout restart deploy/langwatch-app. New JWTs are signed with the new value; JWTs signed before the restart still verify against the previous value.
  4. After 15 minutes, delete the LW_GATEWAY_JWT_SECRET_PREVIOUS key from the Secret, clear jwtSecretPreviousKey, and run helm upgrade again.
LW_GATEWAY_INTERNAL_SECRET has no previous-key slot. Change it in the Secret and restart the app and the gateway Deployments together.

Control plane dependency

The gateway calls these routes on LW_GATEWAY_BASE_URL, signed with LW_GATEWAY_INTERNAL_SECRET: /api/internal/gateway/resolve-key, /config, /changes, /health, /budget-bucket-spend and /realtime-sessions. It also ships spend records to the control plane’s spend ingest. When the control plane is unreachable, virtual keys already in the auth cache keep serving under the stale-while-error rules on Config. Keys not in the cache fail with 503 auth_upstream_unavailable. Budget debits come from the spend records the gateway ships. With spend.enabled: false the gateway serves traffic and debits no budget. The spool lives in the pod’s /tmp emptyDir (scratchSizeLimit, default 1Gi) and is capped at 64 MiB. Alert on gateway_spend_spool_dropped_total: every drop is spend that is never debited.

Graceful drain

On SIGTERM the gateway:
  1. Flips /readyz to 503 {"status":"draining"} and sets the gateway_draining gauge to 1.
  2. Waits shutdown.preDrainWaitSeconds (default 5) so the endpoint controller removes the pod from the Service.
  3. Stops the HTTP listener and gives in-flight requests up to shutdown.timeoutSeconds (default 60) to finish, then stops the other services in reverse start order. The spend spool closes after the listener, so requests that finish during the drain still record spend.
terminationGracePeriodSeconds must be at least preDrainWaitSeconds + timeoutSeconds + 10. The chart refuses to render otherwise. It also refuses the retired keys shutdown.preDrainWait and shutdown.timeout. A streaming response can run up to 14 minutes, the provider request timeout. With the default timeoutSeconds: 60 the gateway logs graceful_shutdown_shorter_than_max_stream_duration at boot, and a stream still open after 60 seconds of drain is cut. To finish long streams on every rollout:
Watch gateway_draining together with gateway_in_flight_requests. During a healthy drain the in-flight count falls to 0. A flat count past the window is a stuck handler, and the kubelet kills the pod when the grace period ends.

NetworkPolicy

networkPolicy.enabled is false by default. When enabled the chart renders one deny-by-default policy: Replace egressToProviders with explicit CIDR blocks when your policy requires a pinned provider egress set. Check the namespace labels on your cluster before enabling: the defaults match upstream ingress-nginx and kube-prometheus-stack. Confirm the policy renders:

Pod security

The pod runs as user and group 65532 with runAsNonRoot, a read-only root filesystem, all capabilities dropped, the RuntimeDefault seccomp profile and automountServiceAccountToken: false. The only writable path is the /tmp emptyDir. Adjust podSecurityContext and containerSecurityContext only if an admission policy on your cluster requires it.

Roll back

Every new gateway pod starts with an empty auth cache and resolves each virtual key from the control plane on its first request.

Common failures

More symptoms are on Troubleshooting.
Last modified on September 6, 2026