Time‑Travel Debugging in FaunaDB: Point‑in‑Time Reads Without Extra Infrastructure
FaunaDB’s built‑in temporal queries let you read any document or index as it existed at a specific timestamp, enabling audit trails and time‑travel debugging without extra infrastructure. This post shows how At() and Events() work, a concrete example, and the storage trade‑offs.
09 Jan 2026, 21:59 UTC

The problem: you need yesterday’s data, not a backup
When a bug report arrives — “the user’s balance was wrong on 2024‑03‑12” — the usual response is to restore a nightly snapshot, spin up a staging cluster, and hope the schema hasn’t drifted. That workflow is slow, fragile, and often impossible for regulated audit trails that require immutable history.
FaunaDB solves this differently: every write automatically creates a versioned document, and the query engine can execute any read at an arbitrary timestamp. No separate read replicas, no eventual‑consistency window, no extra infrastructure.
How temporal queries work under the hood
- Automatic versioning – Each document write (create, update, delete) stores a new version tagged with a transaction timestamp (
ts). The collection settinghistory_dayscontrols retention (default 30 days,null= unlimited). At()for point‑in‑time reads –At(Time('2024-03-12T12:00:00Z'), q.Get(ref))runs the inner query against the database state exactly at that instant. The read is strongly consistent because it uses the same distributed transaction engine as current reads.Events()for full history –Events(ref)returns a paginated stream of{ action, document, ts }objects (create, update, delete). Pagination usesafter/beforecursors tied to the event timestamp.- Temporal indexes –
At(ts, q.Paginate(q.Match(index, term)))yields the index entries as they existed atts, enabling point‑in‑time reporting without materialized views. - ABAC inherits temporal context – User‑defined functions and role predicates automatically see the historical data at the caller’s timestamp, so no extra logic is needed for historic permission checks.
Worked example: debugging a balance discrepancy
Assume a collection accounts with history_days: 90. A user reports an incorrect balance on 2024‑03‑12. The steps below run in the Fauna Shell (or any FQL client) with an admin key that has read permission on accounts.
// 1. Locate the account reference
let ref = q.Ref(q.Collection('accounts'), '3029481234567890')
// 2. Read the balance as it existed at noon UTC on the day in question
let pastBalance = q.At(
q.Time('2024-03-12T12:00:00Z'),
q.Select(['data', 'balance'], q.Get(ref))
)
// 3. Execute
pastBalance
// → { "@ts": "2024-03-12T12:00:00.123456Z", "balance": 142.57 }
If the value differs from the current balance, you can walk the full history:
q.Paginate(q.Events(ref), { size: 20 })
// Returns pages of { action: 'create'|'update'|'delete', document: {...}, ts: ... }
Each page’s after cursor lets you fetch the next chunk. The ts field is the transaction timestamp, monotonic across events.
Trade‑offs and limitations
| Aspect | Impact |
|---|---|
| Storage cost | Every version consumes billable storage (GB/month). Unlimited history on a high‑write collection can grow quickly; monitor the billing dashboard after enabling history_days: null. |
Cross‑collection At() | A single At() call cannot span multiple collections at different timestamps. Wrap each collection reference in its own At() if you need divergent points in time. |
| GraphQL exposure | The native GraphQL API does not surface temporal queries. Expose them via a UDF annotated with @resolver or call FQL directly from your backend. |
Retroactive history_days changes | Altering history_days only affects future versions; existing versions keep their original TTL. |
| Delete does not purge | Remove() adds a delete action event; the document’s history remains until the retention window expires. |
Actionable next steps
- Enable a short retention (e.g.,
history_days: 1) on a test collection, write a document, update it, then runAt(TimeSubtract(Now(), 1, 'hours'), q.Get(ref))in the Fauna Shell to verify the prior version returns. - Query
Events(ref)and confirm the response includescreate,update, anddeleteactions with monotonically increasingtsvalues. - If you plan unlimited history on a production collection, project storage growth by measuring version size × write rate, then set a budget alert in the Fauna billing dashboard.
- Wrap any temporal reads needed by GraphQL consumers in a UDF and expose it with
@resolverso front‑end code stays unchanged.
With these patterns you get audit‑grade time travel, point‑in‑time reporting, and fast debugging — all without managing a separate history store.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.