Backups and restore

Backing up Flux's event log, artifacts, and encryption key — restore procedure, drill cadence, and what's NOT recoverable from a snapshot.

A Flux backup has three planes. Miss any one and your restore fails — silently in the case of secrets, loudly in the case of artifacts that the event log still references.

What to back up

Plane A: event log and catalog

This is your database. SQLite (sqlite:///.flux/flux.db by default) for dev, Postgres for prod. Everything Flux remembers lives here: workflow source, every state transition, schedules, principals, API keys, encrypted secrets, and worker registration data.

The schema is Alembic-managed (flux/migrations/); Flux migrates the database to the latest revision automatically on first connect, and flux db current shows the revision a backup was taken at. The tables:

If you back up the database, you back up all of this in one consistent snapshot. Take a fresh snapshot before every Flux upgrade — migrations modify schema in place, and the backup is the only rollback path (see Upgrades and migrations).

Plane B: output storage / artifacts

Depends on which OutputStorage you’ve configured. The default is InlineOutputStorage — task outputs are pickled into the execution_events.value column. If you’re on the default, Plane A backs up your artifacts too. No separate artifact store exists.

If you’ve switched to LocalFileStorage, outputs live as files under <home>/<local_storage_path>.flux/.data/ by default. Flux 0.56.0 ships only InlineOutputStorage (the default) and LocalFileStorage. S3 storage requires shipping your own OutputStorage subclass. If you’ve added one, back up its bucket separately.

Check OutputStorageReference.storage_type on a recent execution if you’re not sure: inline means Plane A is sufficient; local_file (or a custom storage_type you’ve implemented yourself) means you need a separate plane.

Plane C: secrets encryption key

FLUX_SECURITY__ENCRYPTION__ENCRYPTION_KEY — the value that derives the AES-GCM key for the secrets table. The same key also HMAC-signs the runtime pickle columns — execution input/output, event values, schedule input — so a database restored under a different key fails integrity checks on that data, not just on secrets. Back the key up separately from the database. Compromising Plane A and Plane C together means your secrets are plaintext to anyone holding both.

Out-of-band channels for this: a cloud KMS, a sealed envelope, a corporate password manager. Not a file next to the dump.

What NOT to back up

Worker process state, in-flight memory, SSE connections, the module-source cache. Workers re-register on restart, claim work fresh, and recompile workflow modules from the catalog. None of it is durable, none of it needs to be.

Postgres backup

Two paths depending on volume and RPO:

Logical (low-volume installs):

pg_dump --format=custom --file=flux-$(date +%F).dump $DATABASE_URL

Restore with pg_restore --clean --if-exists --dbname=$DATABASE_URL flux-2026-05-14.dump. Simple, slow on large stores, gives you a point-in-time snapshot only as fresh as the last dump.

Physical (high-volume or tight RPO): enable WAL archiving and take a periodic pg_basebackup. This gives you point-in-time recovery (PITR) to any moment between the base backup and the latest archived WAL segment. Required if your RPO is measured in minutes.

SQLite backup

The server keeps SQLite in WAL mode (set via PRAGMA journal_mode=WAL in SQLiteRepository). Do not copy flux.db while the server is running — you’ll get a database without its WAL tail. Use the online backup API instead:

sqlite3 .flux/flux.db ".backup '/backups/flux-$(date +%F).db'"

This handles the WAL correctly and produces a consistent file.

Artifact backup

For LocalFileStorage, rsync the configured directory:

rsync -a --delete .flux/.data/ /backups/flux-data/

Run it after the DB snapshot — if a new execution writes an artifact between the two backups, you want the file present, not missing.

For custom S3-backed storage (not shipped by Flux 0.56.0 — only relevant if you’ve subclassed OutputStorage yourself), lean on the platform: enable versioning on the bucket and cross-region replication. Flux writes objects under deterministic reference IDs, so versioning gives you point-in-time semantics for free.

Restore procedure

Order matters. Restore inconsistencies (DB references an artifact you haven’t restored yet, or a key you don’t have) surface as decoding errors at runtime, not at startup.

  1. Stop the server and all workers. No new writes during restore.
  2. Restore the database to a known consistent state (the latest dump, or a PITR target).
  3. Restore artifacts (if Plane B applies). DB references must resolve.
  4. Restore the encryption key into the environment (FLUX_SECURITY__ENCRYPTION__ENCRYPTION_KEY) or config. It must be the original key: without it every read of the secrets table raises, and the HMAC-signed pickle columns (execution values, event payloads) fail integrity checks.
  5. Start the server. It migrates the restored database to the current schema revision automatically if the backup predates the running Flux version. Hit /ready and confirm it returns 200.
  6. Start workers. Watch them register and pick up the SSE stream.

What is NOT recoverable

Executions that were in-flight at the moment the backup ran. The event log is consistent up to the snapshot, but a task that was mid-execution when the dump completed has no resume point — the worker that owned it is gone, its in-memory state is gone.

The replay model salvages most of this: tasks that completed (TASK_COMPLETED event present) before the snapshot replay deterministically on resume; tasks that were running (TASK_STARTED without a matching TASK_COMPLETED) re-run from the start. That’s safe for idempotent tasks and unsafe for tasks with external side effects. If your workflow writes to a non-idempotent system, audit recently-resumed executions after a restore.

Restore drills

Monthly, against a non-prod environment. The drill is not the dump — it’s the restore. Test:

A backup that has never been restored is a hypothesis, not a backup.

What can go wrong