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
helm3. - 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 The image is
gateway block to your umbrella values.yaml: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
helm upgrade rolls the app and the gateway together. The gateway Deployment uses a RollingUpdate with maxSurge: 1 and maxUnavailable: 0.4
Verify
What the chart renders into the pod
The chart passes these environment variables to the container. Every other key in the gatewayvalues.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 againstLW_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.
- Edit the Secret: copy the current
LW_GATEWAY_JWT_SECRETvalue into a new keyLW_GATEWAY_JWT_SECRET_PREVIOUS, then replaceLW_GATEWAY_JWT_SECRETwith the new value. - Set
gateway.secrets.jwtSecretPreviousKey: LW_GATEWAY_JWT_SECRET_PREVIOUSand runhelm upgrade. The gateway pods restart with both values. Wait forkubectl -n langwatch rollout status deploy/langwatch-gateway. - 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. - After 15 minutes, delete the
LW_GATEWAY_JWT_SECRET_PREVIOUSkey from the Secret, clearjwtSecretPreviousKey, and runhelm upgradeagain.
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 onLW_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
OnSIGTERM the gateway:
- Flips
/readyzto503 {"status":"draining"}and sets thegateway_draininggauge to 1. - Waits
shutdown.preDrainWaitSeconds(default 5) so the endpoint controller removes the pod from the Service. - 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:
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 group65532 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
Common failures
More symptoms are on Troubleshooting.