Skip to content
SRE & DevOpsDeep Dive Published Updated 6 min readViews unavailable

Cloudflare D1 Migrations and Time Travel: Safe Schema Changes and Point-in-Time Recovery

Use Wrangler's D1 migration ledger, local rehearsal, explicit remote targeting, and destructive-restore safeguards to operate SQLite-backed Worker databases.

Cloudflare D1 is a managed SQL database built around SQLite and integrated with Workers and Pages through bindings. D1 removes the need to operate a database server, but it does not remove schema ownership, release sequencing, data validation, or recovery planning. The production question is not merely whether a migration command returned success; it is whether the intended database received the reviewed migration, whether application code tolerates the schema transition, and whether the team can recover from a destructive mistake.

Wrangler’s migration system stores ordered SQL files in a migrations directory and records applied migrations in a database table (by default, d1_migrations). It can create migration files, list unapplied migrations, and apply remaining migrations. This ledger makes repository history part of database state: changing an already-applied migration file can make local and remote schemas diverge, so treat applied files as immutable and add a new forward migration for later changes.

Create a migration and rehearse it locally

Generate a named migration and inspect its SQL before execution:

npx wrangler d1 migrations create APP_DB add_order_state
npx wrangler d1 migrations list APP_DB --local
npx wrangler d1 migrations apply APP_DB --local

Replace APP_DB with the configured database binding or database name in the Wrangler configuration. Prefer the stable database name when there is a risk that environment-specific binding names could target different databases. If an ORM writes each migration into a nested directory, configure migrations_dir and migrations_pattern to match that layout; Wrangler’s own migrations create command writes only top-level files, so generate nested ORM migrations with the ORM tool.

Run tests against the local D1 database with representative data. Assert invariants after the migration, not just successful SQL execution: row counts, uniqueness, foreign-key relationships, expected defaults, query plans for critical paths, and the application’s read/write behavior. Keep migration commands explicit about local versus remote operation. A local success does not prove that the production database has the same migration history or data shape.

For a change that cannot be deployed atomically with the application, use an expand-migrate-contract sequence. First add a backward-compatible column, table, or index; deploy code that can read both old and new shapes and dual-write only where necessary; backfill in bounded, observable batches; switch reads after validation; and remove the old representation in a later release. This is an application/database rollout pattern, not a D1 feature guarantee. Test the specific SQL and runtime behavior in the D1 environment your application uses.

Apply remote changes as a gated release operation

Before a production migration, inspect the Wrangler configuration and confirm which account, database ID, environment, and binding will be targeted. List pending migrations against the remote database, review the exact ordered SQL, check the migration table, and verify the application version that will run against the result. Use a dedicated deployment identity with only the permissions required for the intended account and database operation. Keep production credentials out of logs and shell history.

The command below is deliberately explicit. Wrangler’s --remote flag selects the remote D1 database; do not rely on a local developer’s remembered defaults. Some Wrangler commands prompt interactively, but the D1 documentation notes that in a CI or other non-interactive environment the confirmation step is skipped. CI must therefore enforce an approval or deployment gate before it invokes a remote apply command.

npx wrangler d1 migrations list APP_DB --remote
# After review and the release gate:
npx wrangler d1 migrations apply APP_DB --remote

Where Wrangler environments are used, ensure the deployment and D1 commands resolve the same configuration/environment and database identity. Record the deployed application commit, migration filenames, run result, and target database. Afterward, verify the remote migration ledger and a small set of business invariants through the application or a read-only query. Do not infer data correctness from the command’s green exit status alone.

Design each migration to be safe under the documented migration runner’s behavior and the SQL engine’s semantics. If a migration fails, inspect the error and applied-migration ledger before retrying; do not manually edit the ledger to make CI green. Wrangler reports progress for migration application and the current command documentation describes rollback of a failing migration while keeping prior successful migrations applied, but recovery planning still needs to account for data changes, application compatibility, and the exact current Wrangler version.

Know what D1 Time Travel protects and what it does not

For databases on D1’s production storage backend, Time Travel is enabled automatically and retains point-in-time recovery history. The documented retention is plan-dependent: up to 30 days on Workers Paid and 7 days on Workers Free. Check the live Limits documentation and the database’s backend rather than assuming a retention promise applies to every legacy database. Run wrangler d1 info YOUR_DATABASE and inspect its version; Cloudflare documents version: production for the newer backend that supports the Time Travel API, while version: alpha uses the older snapshot-based backup API.

Use wrangler d1 time-travel info to identify the current bookmark or look up one for a timestamp before considering recovery. A restore is destructive and in-place: it overwrites the database at the selected point, and in-flight queries and transactions are cancelled. The command returns the previous bookmark, which can be used to undo the restore, but that is not a reason to run it casually. No Time Travel clone/fork was documented as available in the source current at this article’s review date.

# Read-only discovery; confirm the database and time before planning recovery.
wrangler d1 info YOUR_DATABASE
wrangler d1 time-travel info YOUR_DATABASE --timestamp="2026-10-05T12:00:00Z"

# Destructive example only: run only under an approved incident procedure.
# wrangler d1 time-travel restore YOUR_DATABASE --timestamp="2026-10-05T12:00:00Z"

Time Travel is not the same thing as a separately controlled, long-retention export. If your recovery point objective requires retaining a portable copy beyond the Time Travel window, follow Cloudflare’s supported export-to-R2 workflow and test restoring the export into a non-production database. Define who can initiate a restore, how writes are quiesced or reconciled, which timestamp/bookmark has incident approval, and how to validate recovered rows before traffic is returned.

Turn recovery into an exercised runbook

Maintain separate procedures for a failed schema migration, accidental data mutation, and application rollback: the right response is not necessarily the same. A code rollback may be unsafe after a destructive schema change, and a database point-in-time restore can discard valid writes that arrived after the selected bookmark. Preserve the event time, exact database identity, bookmark, previous bookmark, migration record, and affected application build as incident evidence.

Perform a scheduled recovery exercise against a disposable database. Create test data, apply a migration, make a controlled erroneous update, inspect the relevant bookmark, restore only the test database, verify both expected data and schema, and document elapsed recovery time and any data loss. Separately export to the longer-term destination if required and verify that the export can be read. A backup command that was never restored is not proven recovery.

Related:

Sources:


Comments