Troubleshooting
Start from what you see. Each entry gives the likely cause, where to check it, and the fix. A short FAQ follows.
On this page
- Install and upgrade
- Helm fails with production requires image.digest
- The migration hook fails or times out
- Sign-in
- The login page won't load, and /api/v1/auth/config returns 503
- You come back from the identity provider and aren't signed in
- Dev sign-in returns 503
- Scans
- A scan finds no tables
- Tables are discovered but fail to scan
- A table stays unreadable after a rescan
- Runs and jobs
- Tables have grades, but no maintenance runs happen
- A job is deferred with Destructive operation requires approval
- A job is deferred with table_locked
- Jobs stay queued and nothing runs
- Daily runs fire twice, at odd times
- Tables show skipped at the end of a nightly run
- Spark lanes
- A lane is OOMKilled or exits with 137
- A Spark pod fails with exec format error
- Creating a Job or Secret fails with 403
- FAQ
- Still stuck
Most answers are on the job itself. Open Runs, then the run, then the job. A deferred or failed job shows its reason there. The commands below assume the release name cryo in namespace cryo.
Install and upgrade#
Helm fails with production requires image.digest#
- Cause
- With
CRYO_ENVIRONMENT=prod, the chart refuses to render until every enabled image is pinned by digest. The same check coversmigration,web,redis,nativeandspark.k8s. - Check
helm template cryo ./deploy/helm/cryo -f cryo-values.yamlnames the missing value.- Fix
- Set that digest. See Install.
The migration hook fails or times out#
- Cause
- Most often a lock. Migrations use
lock_timeout=10s, so a long-open transaction on a Cryo table makes them fail instead of wait. Wrong database credentials, or a missingpgcryptoprivilege on a first install, also fail here. - Check
kubectl -n cryo logs job/cryo-migrate-<revision>. For locks, look for sessions leftidle in transaction:SELECT pid, state, xact_start, query FROM pg_stat_activity WHERE datname = current_database() AND state LIKE 'idle in transaction%';- Fix
- End the blocking session, then run the same
helm upgradeagain. The failed chain was rolled back as one transaction, so nothing is half-applied. See How the migration hook behaves.
Sign-in#
The login page won't load, and /api/v1/auth/config returns 503#
- Cause
- The detail
OIDC issuer discovery failedmeans the API couldn't fetchCRYO_OIDC_ISSUER_URL/.well-known/openid-configuration. - Check
kubectl -n cryo exec deploy/cryo-api -- sh -c 'curl -sS "$CRYO_OIDC_ISSUER_URL/.well-known/openid-configuration"'fetches it from inside the pod, with the pod's own setting.- Fix
- Correct the issuer URL, or allow egress from the cluster to it.
You come back from the identity provider and aren't signed in#
- Cause
Code exchange failed(400) means a wrong client secret, or aCRYO_OIDC_REDIRECT_URIthat doesn't match the URI registered at the provider. A 401 withInvalid bearer tokenafterwards means Cryo rejected the provider's access token. It may not be a JWT, itsaudorissmay not match, or it may carry no Cryo role.- Check
- Decode the access token (it's a JWT) and compare
iss,audand the role claims with OIDC setup. - Fix
- Set
CRYO_OIDC_AUDIENCEto the token's audience, or configure the provider to issue a JWT access token. Then emit a role named exactlyviewer,operatororadmininroles,groupsor one of the other claims Cryo reads.
Dev sign-in returns 503#
- Cause
- This is intended. The dev token route is off in
prod. - Check
/api/v1/auth/configreports"dev_token_enabled":false.- Fix
- Sign in through OIDC. Don't set
CRYO_ALLOW_DEV_TOKEN_IN_PRODto work around it. See The dev token.
Scans#
A scan finds no tables#
- Cause
- If Discovered is 0, the problem is the catalog or the schema filter, not storage. Common causes are a wrong catalog name or warehouse, catalog credentials that can list nothing, or Schemas for maintenance set to schemas that don't exist.
- Check
- Open Scans (
/scans), then the scan, and read its live log. Run the connection test on the source under Settings → Connections. - Fix
- Correct the source and scan again. See The first scan.
Tables are discovered but fail to scan#
- Cause
- Cryo found the tables but couldn't read their files.
AccessDeniedon an S3 path is a credentials problem. On EKS, the usual cause is a ServiceAccount without aneks.amazonaws.com/role-arnannotation. - Check
- The first failure in the scan's live log usually explains the rest. The storage step on the source shows which buckets are readable.
- Fix
- Grant read access, or set
serviceAccount.annotations. See S3 authentication.
A table stays unreadable after a rescan#
- Cause
- Two categories are permanent.
metadata_not_foundmeans the catalog points at a metadata file that isn't in storage.manifest_missingmeans a manifest list the metadata names is gone. A query engine fails the same way.permission_deniedisn't permanent: an IAM change fixes it. - Check
- The table's last scan error, on the table's row in Tables and on its detail page.
- Fix
- The table's owners repoint the catalog entry or restore the table. Rescanning doesn't help. Don't drop the table because of this error alone.
Runs and jobs#
Tables have grades, but no maintenance runs happen#
- Cause
- Scanning never schedules maintenance. A table needs a preset, and no preset is assigned by default.
- Check
- Presets (
/presets) lists each preset, what it applies to and its schedule. - Fix
- Assign a preset. See Maintenance.
A job is deferred with Destructive operation requires approval#
- Cause
expire_snapshots,expire_dataandremove_orphan_filesneed an approval for that table and those parameters. The governor creates the approval request when it defers the job.- Check
- Governance (
/governance) lists the pending approvals. - Fix
- Approve it or reject it. Waiting, requeueing and adding workers don't clear it.
A job is deferred with table_locked#
- Cause
- Another job holds this table's lock, so the two can't rewrite the same table at once. It's usually a job from another run that's still running.
- Check
- Look in Runs for another run with a job still running on the same table. The deferred job is retried on later governor waves.
- Fix
- Usually nothing. It runs when the other job finishes. If the holder will never finish, the governor reaps stale running jobs and their locks after the lock TTL (six hours).
Jobs stay queued and nothing runs#
- Cause
- Either work isn't being admitted, or it's admitted and no worker picks it up. A worker runs one job at a time. A pause, an approval, a budget or ROI refusal, or the engine's slot cap keeps work from being admitted, and more workers don't change that.
- Check
- Each job's deferral reason names the gate that refused it. Control Room (
/control-room) shows active pauses. Then check that the governor and workers are running:kubectl -n cryo get pods -l app.kubernetes.io/component=governor, then the same forworker, and readkubectl -n cryo logs deploy/cryo-governor --tail=50. - Fix
- Clear the gate the reason names. Add workers only when jobs are admitted and waiting with no deferral reason.
Daily runs fire twice, at odd times#
- Cause
- Redis restarted. The scheduler's "last fire" markers live only in Redis, so after a restart a daily preset or the daily scan can fire again within 24 hours. This is a known issue, #177.
- Check
- Compare the extra run's start time with the Redis pod's start time:
kubectl -n cryo get pod -l app.kubernetes.io/component=redis -o wide. - Fix
- Keep Redis stable until #177 is fixed. See Losing Redis.
Tables show skipped at the end of a nightly run#
- Cause
- The run hit its deadline before reaching them. They weren't lost and didn't fail.
- Check
- The run's summary and the skipped jobs' reasons.
- Fix
- Nothing urgent, because the next night picks them up. If it happens every night, add lanes or narrow the preset.
Spark lanes#
A lane is OOMKilled or exits with 137#
- Cause
- The lane pod ran out of memory. The JVM heap is 60% of the pod limit, and the rest is for off-heap memory and the runner. A Java 21 lane can also fail with
OutOfMemoryError: Java heap spaceandRetried waiting for GCLocker too oftenwhile the heap is mostly free. - Check
kubectl -n cryo describe pod <lane-pod>shows the termination reason. The job's error names the table that was running.- Fix
- A table that died mid-run is marked failed and isn't retried on the same pod size. Tables the lane hadn't started go to a replacement lane. Lane pods are capped by
maintenance.laneMaxMemMib, which defaults to 16384.
A Spark pod fails with exec format error#
- Cause
- The Spark image is amd64-only, and the pod landed on an arm64 node.
- Check
- The
nodeSelectoron the Spark wrapper Job, not on Cryo's own Deployments, which are pinned separately. - Fix
- Don't set
kubernetes.io/archtoarm64in the Spark destination's node selector or inCRYO_SPARK_K8S_NODE_SELECTOR. Leave it unset, and Cryo addsamd64.
Creating a Job or Secret fails with 403#
- Cause
- The worker's ServiceAccount can't create the lane Job or the driver Secret. That RBAC is only rendered when
native.enabledorspark.k8s.enabledis true. - Check
kubectl -n cryo get role,rolebindingshows whether a Role exists that grantsbatch/jobscreate andsecretscreate, delete and patch.- Fix
- Turn on the lane you use,
native.enabledorspark.k8s.enabled, pin its image digest inprod, and upgrade.
FAQ#
- How is this different from running
OPTIMIZEon a cron? - A cron runs the same statement against the same tables whatever their state. Cryo scans every table, grades it, and plans only the work it needs, scoped to partitions where it can. It runs operations in a fixed safe order. A governor admits work against your engines' live capacity, and every job leaves an audit row. See How Cryo fits.
- Is it safe to point at production?
- Scanning is read-only. Destructive operations (snapshot expiry, data expiry and orphan-file removal) need an approval for each table by default, and a pause stops new work before it's submitted. Cryo is pre-1.0. Start with a preset on a few tables you own, and read Security first.
- Which engines are supported?
- Trino, Apache Spark (EMR Serverless, or Spark on Kubernetes run by Cryo), and a native engine for compaction, which is in beta. For Flink, Cryo generates a maintenance config but doesn't submit the job. See Engines.
- Which catalogs?
- Iceberg REST, Hive Metastore, AWS Glue, Polaris and Nessie. Unity Catalog and JDBC catalogs are rejected. See Catalog types.
Still stuck#
Search the issues, or open one with the symptom, the job's reason and the relevant log lines. Remove hostnames, credentials and table names you can't share. For a security problem, follow SECURITY.md instead.