Skip to main content
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 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

  1. Beacon arrives at POST /e.
  2. Worker writes a row to the acceptance journal (D1).
  3. Worker enqueues the batch on a Cloudflare Queue.
  4. 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.
  5. Journal row is marked queuedsettled.
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.
  • Retained for 24 months, then purged.

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.