Every event that arrives at api.abtestly.com/e is written to two
places. The first is Cloudflare Analytics Engine, fast and cheap to
query, drives the live counters on the dashboard. The second is the
Exact Experiment Ledger, the canonical store your final numbers
come from.
The ledger is the difference between a dashboard that says
“approximately 12,483 visitors” and one that says “12,483 visitors,
here is the file”.
Why two stores
Analytics Engine is optimized for a huge write rate and a cheap query.
Under very heavy bursts, it can sample its own data, that is fine
when you are updating a live counter, less fine when the number is
going into a report.
The ledger is optimized for correctness. Every accepted event, in order,
retrievable. It is what the results page reads once your test is out
of “very-fast-changing” territory, and what the raw event
export reads for you.
How an event lands
- Beacon arrives at
POST /e.
- Worker writes a row to the acceptance journal (D1).
- Worker enqueues the batch on a Cloudflare Queue.
- Queue consumer:
- Archives the batch as gzipped NDJSON in R2 (partition by
site/experiment/date, one file per batch).
- Projects into the experiment’s Durable Object, updating the
canonical counters.
- Journal row is marked
queued → settled.
If step 3 or later fails, the journal row stays pending and is
retried. The results page reads a completeness field with the count
of pending rows for the current experiment.
The completeness banner
When the ledger has a backlog, the results page shows a banner:
Processing. Your dashboard counters are current. The verdict is
held for a moment while the ledger catches up on the burst.
Two things it does not mean:
- It does not mean data is being dropped. Pending events are on
disk; the queue will process them.
- It does not mean the live counters are wrong. Analytics Engine
is up-to-date; only the ledger-driven verdict is being paused.
A backlog usually resolves in seconds. If a batch fails permanently,
it lands in the dead-letter queue and shows as a warning until
resolved by us.
What the ledger stores
- Every exposure and every goal fire.
- With their full payload, variant key, order id, currency, entry
path, all the segment fields, timestamps.
How long it keeps things
Two stores, on two different clocks, on purpose.
- The canonical ledger: the per-visitor records your results are
computed from are kept for the life of the experiment, along with the
verdicts and per-variant totals derived from them. Nothing ages these
out on a timer. Each row carries a pseudonymous visitor identifier and
coarse cohort fields (device, country, browser, OS, referring host,
campaign), so treat them as personal data even though they carry no
IP address, page URL, or raw user agent string. They are removed when
you reset the experiment’s data, when you delete the experiment, and
when the site is deleted.
- Raw individual events: the compressed event-level archive behind
the ledger is kept for 24 months, then purged nightly. This is what
raw event export and audits read from.
Both windows are stated in the
privacy policy.
If you need the raw event stream to outlive the 24-month archive, run
the async export inside the window and pull the CSV
to your own storage. That gives you the same rows the ledger held,
under your control, retained for as long as you want.
Where the ledger lives in the code
- Acceptance journal: D1 (
acceptance_journal table).
- Cold archive: R2 (
raw/site=…/experiment=…/epoch=…/y=…/m=…/d=…/<batchId>.ndjson.gz).
- Per-experiment canonical projection: Durable Object (
ExperimentLedgerDO,
SQLite inside).
You do not need to know any of that to use it. It is here so nothing
about the ledger is a black box if you go looking.
What you use the ledger for
- Trustworthy verdicts. The results page reads the ledger for its
numbers.
- Raw event export. The export reads the ledger
archives in R2.
- Audits. If a compliance team needs “every event this experiment
saw”, the ledger is the source of truth.
What you do not use it for
- Live counters. Those still read Analytics Engine, the second-by-second
update on the dashboard would not scale to per-experiment DO reads.
The “no sampling” claim, in one sentence
For any experiment you have run, the ledger contains every exposure
and every goal that was accepted. When the results page runs its
statistics, they are computed against that set. There is no sampling
step, at any point, on any surface you make decisions from. Last modified on September 10, 2026