Skip to content

How the burn works

meta matt gennings edited this page Jul 1, 2026 · 1 revision

How the burn works

The single hardest correctness requirement in Cinder is "exactly one reader, ever." This page explains how one database operation delivers that guarantee, and why the obvious alternatives don't.

The problem

A self-destructing note has to be readable exactly once. That sounds simple until you consider concurrency: two people could click "Reveal" on the same link in the same instant, or one person's browser could retry a request that already succeeded. A naive design reads the note, sends it back, and then deletes it — which leaves a window where a second reader can slip in between the read and the delete and get the note too. That window is small, but "small" is not "never," and for a privacy tool "never" is the whole point.

The solution: one atomic operation

Cinder never reads and then deletes. It does both in a single, indivisible database operation — a DynamoDB conditional DeleteItem that returns the item it deleted:

DeleteItem(
  Key: { pk: noteId },
  ConditionExpression: "attribute_exists(pk) AND expiresAt > now",
  ReturnValues: ALL_OLD
)

Read that as one sentence: "delete this note if it still exists and hasn't expired, and hand me back what you deleted." Because it's a single operation, there is no gap for a second reader to exploit. The database serializes concurrent writes to the same item, so if two requests arrive together, exactly one of them satisfies the condition and performs the delete. That one gets the note back. The other finds the condition false (the item is already gone) and gets nothing — Cinder turns that into a 410 Gone.

Why the condition has two parts

attribute_exists(pk) is the "hasn't been read yet" check — once the item is deleted, it fails. expiresAt > now is the "hasn't expired yet" check, and it's there for a subtle reason.

DynamoDB has a built-in time-to-live feature that deletes expired items automatically — but it's best-effort. Expired items can linger for up to a couple of days before the sweep removes them, and while they linger they are still returned by reads. If Cinder relied on time-to-live alone, an expired note could occasionally be served in that window. The expiresAt > now guard closes that hole: an expired note fails the condition and returns 410, even if the automatic sweep hasn't reached it yet. Time-to-live is a janitor, not the lock.

What each reader experiences

Situation Database result What the reader sees
First to click Reveal Condition true, delete succeeds, item returned The note, then "This note is now destroyed"
Second (racing) reader Condition false, no delete "This note is gone" (410)
Opening an already-read link Condition false (item gone) "This note is gone" (410)
Opening an expired link Condition false (expiresAt guard) "This note is gone" (410)

Proven, not asserted

This isn't a hand-wave — it's tested. One of Cinder's tests fires three concurrent burns at the same note and asserts that exactly one returns the note and the other two get nothing. Another stores a note with an already-past expiry and asserts it's never served. Those tests run against a real (local) DynamoDB, so they exercise the actual database semantics, not a mock. See api/test/store.test.mjs in the repo.