Before You Upgrade
- Check release notes for breaking changes at github.com/langwatch/langwatch/releases
- Back up your databases: see Backups
- Test in a staging environment before upgrading production
- Keep
CREDENTIALS_SECRETunchanged. A new value makes stored provider keys and integration secrets unreadable and invalidates API keys. To replace it, follow Rotating the credentials secret as a separate change, not during an upgrade - Write the version 2 queue format first. This release reads only version 2 GroupQueue jobs and drops a job an older release wrote in the earlier format. Before upgrading, set
GROUP_QUEUE_ENVELOPE_WRITES_ENABLED=trueon the running release and roll it out, or pause traffic and let the queue drain. After the upgrade, remove that variable andES_FOLD_TRUST_ABSENT_MISS: this release reads neither
Database privileges
The upgrade keeps its ledger in its own Postgres schema,<schema>_upgrade_ledger, beside the installation’s. The DATABASE_URL role needs CREATE on the database to create it, or you can create that schema beforehand, owned by the role. Without either, the api and the worker refuse to start and name the missing privilege.
Automatic identity and access migration
Identity and access migrations run automatically after each upgrade, organization by organization: the preparation step runs them right afterpnpm task upgrade, and the workers run them again every hour. A tenant whose
authorization, identifier, and existing SSO data migrate cleanly is switched
over. No preparatory release or manual migration pass is required.
A tenant whose historical data disagrees with the migrated result is held
on its existing legacy sign-in path: the migration leaves that tenant’s data
untouched, and the app still starts and serves it as before. A tenant whose
migration throws is parked the same way, also without blocking startup.
Both are retried automatically. The next upgrade and the workers’ hourly pass
re-drive them, and an operator can also trigger a pass, or target one
organization, without waiting. See
Check the migration results below.
The preparation step still fails in three cases: the migration runner itself
fails outright, the queue behind it fails to drain, or the fleet makes no
progress for 25 passes (about two minutes: for example because Redis is
unreachable, or another process holds every claim).
In any of them it exits non-zero, which fails the upgrade (the Helm
pre-upgrade Job where one renders, otherwise the workers), and the new api is
not ready until the installation is current, so the previous version keeps
serving; the next run retries from scratch.
Keep the current identity-provider configuration, callback URLs, and
CREDENTIALS_SECRET during the upgrade. The migration preserves the existing
SSO route; switching providers remains a separate operation.
Take a fresh database backup and deploy normally. The diagnostics below help
resolve held or parked tenants; they are not a routine preflight.
Check the migration results
Sign in with an operator account fromADMIN_EMAILS and open Ops >
Upgrades > Tenant migrations (/ops/upgrades?tab=tenants). It lists every
registered tenant migration with its Finalized, Held and Parked counts, and
lets you search for an organization to inspect its status, target a migration
run for it, or start a fresh pass immediately rather than waiting for the next
restart.
The live connection indexes deliberately refuse duplicate slots rather than
silently choosing a connection. REJECTED, DRAFT, and CLAIMED still occupy
slots; only DISCARDED and TORN_DOWN release them.
ScimDirectoryUser already exists, run the same query with that table as
well. The ownership migration derives organizationId from the connection
and refuses missing connections rather than guessing a tenant. It also
backfills verified-domain routing for existing live connections of every source.
SCIM now keeps each organization’s directory profile and active state separately
from the shared login account. A directory rename does not change the primary
sign-in email, and deactivation removes access only to that organization.
Inactive directory users remain available through SCIM reads. Group external
identifiers are unique within each directory; legacy groups retain their
organization-wide namespace.
Directory usernames remain unique within each organization, ignoring case and
surrounding whitespace. Deleted resources release their usernames. The migration
refuses conflicting existing usernames rather than merging accounts. If both
directory identity tables already exist, this query must also return no rows:
ScimExternalId, use only that table’s
SELECT in the claims expression. Resolve any conflicts before upgrading.
The migration preserves existing global account disables. It cannot distinguish
an earlier SCIM disable from an administrator’s security action, so it does not
automatically reactivate those accounts. Review any historical disable through
the administrator account controls when restoring access.
Docker Compose
migrate service; if your compose file predates this, copy the current one from infra/compose.yml. The workers must be enabled.
Helm Chart
--timeout fails the release part-way through a migration.
Pin a Specific Version
Run the upgrade
Each way of running LangWatch runs the upgrade for you:
To run it yourself, from the new image with the app’s environment (
DATABASE_URL, CLICKHOUSE_URL, REDIS_URL):
pnpm task upgrade (Postgres, ClickHouse and LangWatchQL, under one lease, so a second run waits for the first). It is safe to run again: on a current installation it applies no step and leaves the schema as it is. To see what it would do first, run cd /app/apps/tasks && pnpm task upgrade plan; pnpm task upgrade status shows the ledger. Both take --json.
Exit codes and what to do
pnpm task upgrade exits 0 when it is done and otherwise one of two non-zero codes, and its last two log lines name the cause and the next action. A refused or failed run is recorded, so Ops > Upgrades and upgrade status show it.
Every
1 is safe to retry: the ledger remembers what is done.
When a PostgreSQL migration fails
PostgreSQL applies a migration one statement at a time, so a statement cancelled by the 2 secondlock_timeout leaves its earlier statements applied and a failed row in _prisma_migrations. Every migration written after 20261006180001_gateway_realtime_session_metering is written to survive a second run. For those, upgrade marks the failed row rolled back itself (prisma migrate resolve --rolled-back <migration>), waits and applies it again, up to 3 attempts with a 2 second then 4 second wait.
A failed row an earlier run left is cleared the same way before the schema is applied.
An older migration, or a failed one this image does not ship, stops the run with failed_prisma_migration. Check what it left behind, run prisma migrate resolve --rolled-back <migration> if none of it remains (or --applied <migration> if you finished it by hand) with the app’s DATABASE_URL, then run the upgrade again. No migration has rolled forward, so the previous version keeps serving.
Large tables: build indexes ahead of the upgrade
Two schema changes touch a whole large table while the upgrade runs.AuditLogunique index (170500_audit_log_idempotency_key): PostgreSQL builds it in place and holds a lock that blocks audit writes for the build. On a large table the build can outlast the 2 secondlock_timeoutand fail. Build the index yourself first, during a quiet period, withCREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTSusing the index name the migration declares. The migration then finds it and moves on.- ClickHouse
trace_summariesindex (00103_trace_summaries_updated_at_index): the index is added and then materialised over every existing row. The mutation runs in the background and competes with merges for IO, and laterALTERstatements on that table queue behind it. Watchsystem.mutationsand run the upgrade at a quiet time on a large table.
What the upgrade does
- In release order. From the release the ledger records to the release of the image, the upgrade plans each release’s PostgreSQL migrations, then its ClickHouse migrations on every ClickHouse target (the shared one and each private dataplane), then its blocking data steps. An upgrade by one release applies its schema in one pass, then its blocking steps, as before. An upgrade across several releases steps through them: it says so, then for each release it names the release, applies the PostgreSQL migrations up to and including it, says how many it applied, and runs that release’s blocking steps before it moves on, so a blocking step sees the PostgreSQL schema of its own release. While stepping, Prisma’s own output is shown only when a migration fails. ClickHouse migrations step the same way: each release applies them on every ClickHouse target up to its own last one. Migrations the image ships that no release lists yet are applied in one pass at the end. A ClickHouse target that fails is reported after the others have run, and fails the upgrade.
-
Its own record first. The upgrade keeps its ledger, its lease and the list of running processes in their own PostgreSQL schema,
<your schema>_upgrade_ledger(public_upgrade_ledgerby default), which it creates before anything else. The database user inDATABASE_URLneedsCREATEon the database for that; if it has none, create the schema beforehand with that user as its owner. - From any version. Upgrade to the latest release from any version; the worker rolls through every step. There is no release to stop at first. Releases and support explains which releases get fixes.
-
Short locks and retries. PostgreSQL statements run with a 2 second
lock_timeout. A statement that waits longer fails instead of blocking your traffic. The upgrade makes up to 3 attempts at a failed schema apply (waiting 2 seconds, then 4), then exits; the previous version keeps serving, and running it again resumes where it stopped. -
One run at a time. The upgrade holds a lease in its ledger, renews it every 15 seconds and loses it after 60 seconds without a renewal. A second run waits up to 10 minutes for a live holder, saying so every 30 seconds, then exits
3. - Background steps. Data copies that do not block the release run on the workers after the upgrade. A step that must not run while an older version still writes waits until every running app and worker is on a version that knows it.
ClickHouse is required
The app and the workers refuse to start without ClickHouse. An installation with noCLICKHOUSE_URL, or one where the upgrade never applied the ClickHouse migrations, stays refused by name until pnpm task upgrade has run against a reachable ClickHouse.
While it runs, and after
Every running app and worker records itself in the upgrade ledger’s serving roster as it starts, refreshes that record every 15 seconds, and removes it on a graceful stop. A record not refreshed for 60 seconds counts as gone. The upgrade uses these records to tell when every older version has stopped. A process that cannot refresh its own record for 60 seconds stops serving until a refresh succeeds again, so it never keeps writing unseen. To follow an upgrade:
The installation is in one of seven states: Up to date, Finishing in background (blocking steps done, background steps still running), Behind (this image declares steps the ledger has not run), Never upgraded (an empty ledger), Upgrading, Rolled back (an older version is running beside newer steps) and Needs attention (a step or run failed).
Reading the console
The upgrade logs aslangwatch:tasks:upgrade. Every progress line carries four fields next to its message: phase, waitingOn (what it is blocked on, with a placeholder word when the run is not waiting), elapsedMs since the run began, and next (what you can do). A password, token or connection-string credential is masked in every line. These are the line families, in the order a run writes them:
A serving app or worker writes its own lines before it serves:
A serving process that cannot read the ledger names
DATABASE_URL in its refusal.
Upgrade settings
For the split into api, worker and tasks applications, see the chart’s upgrade notes.
app.features.skipEnvValidation is inert: no process reads SKIP_ENV_VALIDATION any more.
Rollback
A rollback never runs migrations and never needs a down migration. Every migration in the supported window only adds, so the previous version runs on the upgraded schema as it is. The pre-upgrade Job, where one renders, runs onhelm upgrade only, never on helm rollback.
- The previous version starts on the newer schema.
- The Upgrades page shows Rolled back while the older version runs. Background steps that an older version can undo are reopened, and run again after you upgrade again.
- Data the newer version wrote stays. Features the older version does not know about are unavailable until you upgrade again.
Helm
Docker Compose
Pin image tags to a specific version instead oflatest:
Breaking Changes
Internal authentication for the NLP service
The app can now authenticate itself to the NLP service with a shared credential,LANGWATCH_NLP_INTERNAL_SECRET. The app sends it on every NLP request, and the NLP service refuses requests that do not carry it. With the variable unset on the NLP service, it accepts unauthenticated requests as before.
An upgrade requires no change, and an install that never sets the variable keeps working. Setting it is recommended, and the value has to be the same on both sides:
-
Docker Compose: the app, the workers and
langwatch_nlpall read.env, so add oneLANGWATCH_NLP_INTERNAL_SECRETline there (openssl rand -hex 32) and restart the stack. -
Helm chart: with
autogen.enabled=truethe chart generates the value into the app Secret and points the app, the workers and the NLP Deployment at the same key, so a default install needs no change. A value already in that Secret is kept, so an upgrade does not rotate it. If you supply the app Secret yourself, add aLANGWATCH_NLP_INTERNAL_SECRETkey to it and roll the three Deployments. Until you do, the key is absent, the variable stays unset on all three, and the install carries on.
langwatch_nlp:5561, which is what LANGWATCH_NLP_SERVICE already points at. If you were calling port 5561 from the host, add a ports entry for langwatch_nlp in your own compose override.
LangWatch AI Gateway Ingress moved to the app Ingress (Helm)
The gateway sub-chart used to create its own Ingress on every install, with classnginx, host gateway.langwatch.ai, TLS secret gateway-tls, a letsencrypt-prod cert-manager issuer and ingress-nginx streaming annotations. It now renders no Ingress. The gateway.ingress.* values are gone.
To keep publishing the gateway through an Ingress, set its host on the app Ingress. The chart renders a second Ingress, <release>-gateway-ingress, with the app Ingress className, labels and annotations:
className: nginx the chart still adds the streaming and timeout annotations to the gateway Ingress, and now sets a 32 MB body limit. The paths are fixed at /v1 (Prefix) and /health (Exact). For other paths, such as /v1beta, write your own route. If you relied on a cert-manager issuer only on the gateway Ingress, add it to ingress.annotations or ingress.gateway.annotations.
A values file that still sets gateway.ingress.* fails to render with a message naming ingress.gateway.host, so the Ingress cannot disappear on upgrade without an error. gateway.ingress.enabled: false on its own is accepted. If you route to the gateway another way, remove the block and set gateway.publicUrl. See Routing and ingress controllers.
The gateway PodDisruptionBudget now uses maxUnavailable: 1 and renders only when the gateway runs more than one pod. The app, workers, NLP and LangEvals budgets also render only above one replica. The chart refuses a budget that leaves no pod evictable, such as minAvailable equal to replicaCount or maxUnavailable: 0.
Helm installs with autogen off and Argo CD
Withautogen.enabled=false, chart-managed Redis and PostgreSQL now need redis.auth.existingSecret and postgresql.auth.existingSecret (or a password value). The chart used to generate these passwords, and under Argo CD a generated password changed on every sync. A plain helm upgrade of a release that already has the chart’s own <release>-redis and <release>-postgresql Secrets keeps them.
Under Argo CD, first copy the password PostgreSQL currently accepts into a Secret of your own with another name: Argo CD prunes the chart’s Secret once existingSecret is set.
The LangWatchQL render Job is now named <release>-lwql-access-render-<hash> from its spec, and the install Job no longer expires. Argo CD no longer recreates it every few minutes or fails the sync on an immutable Job. Missing LangWatchQL passwords no longer block ClickHouse: the Job logs the missing keys and LangWatchQL stays off.
Keep Argo CD pruning on, so a new spec replaces the old Job; without it, delete old render Jobs before a rollback, or the rollback finds its Job already complete and does not render again.
The app now reads the gateway keys named by gateway.secrets.internalSecretKey and gateway.secrets.jwtSecretKey, the same keys the gateway pod reads.
3.16 or 3.17 to 3.18: Sign-in and SSO
The upgrade keeps existing accounts working with no manual step, apart from the cases below. A Helm upgrade from 3.16.0 with email and password users, an Okta-configured deployment and domain SSO was tested end to end; the Azure AD account move is covered by tests only:- Password users sign in with their existing password on the new email-first sign-in page.
- SSO users land on their existing user and organization. The callback URL stays
/api/auth/callback/<provider>, so the identity provider registration does not change. - Azure AD accounts move to the key the new sign-in looks them up by on each user’s first sign-in after the upgrade. There is no SQL to run.
- Browser sessions and API keys keep working.
- The identity migrations run on first start. Check Ops > Upgrades > Tenant migrations afterwards; see Check the migration results.
--timeout 45m on helm upgrade gives it room to start. A migration that cannot finish during the preflight stays gated and does not block startup, so check Ops > Upgrades > Tenant migrations once the rollout completes. The app can restart once or twice while the bundled ClickHouse restarts on its new image; that is expected.
Provider setting. The chart now passes app.nextAuth.provider to the app as AUTH_PROVIDER. NEXTAUTH_PROVIDER is still read and logs a deprecation warning. If you select the provider with NEXTAUTH_PROVIDER in app.extraEnvs, the chart leaves the setting to you, as before. Outside the chart, set AUTH_PROVIDER; when both are set it wins.
Moving password users to SSO. An SSO sign-in joins an existing password account only once its address is confirmed. See Existing email and password accounts for how linking works in the current release.
Okta and Auth0. The id token is now verified: its iss must equal the issuer in the provider’s discovery document. A provider behind a proxy that rewrites one but not the other stops signing people in until the two agree.
Dataset Object Storage (Helm)
Datasets moved from PostgreSQL to object storage, with a one-time automatic migration on upgrade. Helm deployments running more than one worker replica with the default local storage must either scale workers to 1 or configure S3-compatible storage before upgrading. See the dataset storage migration guide for the upgrade checklist, storage options, and rollback notes.v1.x, v2.x to v3: ClickHouse Migration
LangWatch v3 replaces Elasticsearch/OpenSearch with ClickHouse as the primary data store. This is a zero-downtime migration, Elasticsearch and ClickHouse run side-by-side while you migrate historical data. See the full migration guide for step-by-step instructions covering infrastructure setup, data migration, and cleanup.Helm Chart Pre-1.0.0 to 1.0.0+
If upgrading from a Helm chart version before 1.0.0, the PostgreSQL PVC naming changed. To preserve your data:Known Issues
3.20.1 drops OTLP/JSON spans with no attributes
LangWatch 3.20.1 drops an OTLP/JSON span that has noattributes array, but still answers 2xx, so the SDK reports success. The api logs Error parsing OTLP span, dropping once per lost span. OTLP allows a span with no attributes, and later releases accept it. Spans sent to 3.20.1 this way are not recovered by upgrading; upgrade to stop the loss.
Version Compatibility
Getting Help
If you encounter issues during an upgrade:- Check pod logs:
kubectl -n langwatch logs deploy/langwatch-app --tail=100 - Check the Troubleshooting guide
- Open an issue at github.com/langwatch/langwatch/issues
- Contact support