The mental model

View Source

Five things a host can ask Aurora Meter to do. They look similar and they are not, and picking the wrong one is the most expensive mistake this library lets you make. This page separates them, says what each costs, and says what happens when the machine dies in the middle.

Nothing here is new behaviour. Every row links to the page that states the guarantee and, through it, to the named test that proves it.

The table

You want toUseIt isCost on the callIf the VM dies now
count something cheap and approximateAuroraMeter.track/4a buffered ETS incrementno database writeeverything since the last acknowledged flush is gone
stop someone at a limitAuroraMeter.with_quota/4 or reserve/3local admission, atomic in ETSETS onlythe reservation stays occupied on that node until the period ends or the Store restarts
record a fact you will invoiceAuroraMeter.record/4one database transactionone round tripeither nothing happened, or it happened once and a retry of the same id says so
take or hold moneyAuroraMeter.Credits.with_credits/4, hold/4, settle/3one transaction under the wallet's row lockone round trip plus lock waitthe hold survives and Credits.reconcile_holds/1 closes it, if you have configured a reconciler
do periodic workan Oban workerat least once, with idempotent effectshost scheduledthe job runs again and the effect happens once

Everything in the first two rows lives in memory. Everything in the next two lives in Postgres. That is the whole distinction, and the rest of this page is why it matters.

Why a quota is not a fact

A quota answers "may I?". A fact answers "what happened?".

with_quota/4 reserves against a counter in ETS, runs your function, and commits the reservation on a normal return or releases it on a raise, a throw or an exit. It is atomic on one node: under any concurrency, a hard limit of n admits exactly n. It is not atomic across a cluster, and it does not write anything you can invoice from. See Guarantees G1 to G5.

record/4 writes one row in one transaction, keyed on an id you supply. A retry of the same id is reported as a duplicate rather than applied again, and a different payload under an id already used is reported as a conflict rather than accepted quietly. That is what an invoice can be built from. See Guarantees G10 and Correctness I06 and I07.

The consequence people trip over: with_quota/4 on a feature whose reporting source is :events gates the work in ETS while the billable fact is the separately committed event. Both are correct and they are not the same number at the same instant. Metering and Correctness I08 set out which source answers which question.

Why a fact is not money

A fact is free to write: one insert, one projection delta, no contention beyond the row it creates.

Money is not. Every credit operation serialises on that wallet's balance row, because conservation is a per-wallet property: the balance equals the sum of that wallet's transactions, and there is no way to check sufficiency and spend in one step without holding the row. That is a lock wait under load, and it is the price of not spending the same micro-dollar twice. See Credits and Correctness I10 and I11.

So: count usage with track/4, gate it with with_quota/4, record the invoiceable fact with record/4, and move money with Credits. Four different questions, four different costs.

Where Aurora Meter Pro joins

Pro exports facts to Stripe and settles payments. It never counts. The counting, the plans, the quotas, the events and the credit ledger are all in the free core and stay there; Pro is the settlement layer on top.

A host with no Pro keeps every one of the five operations above. What it does not get is the Stripe exporter, the delivery outbox, reconciliation against the provider, the payment rail for top-ups, and the Pro dashboards. See Free vs Pro.

Choosing

Do you need the number to survive a crash?
├─ No  ──────────────────────────────►  track/4        (buffered, ETS)
└─ Yes
   │
   Is it money leaving or entering a balance?
   ├─ Yes ──────────────────────────►  Credits          (row lock, transaction)
   └─ No
      │
      Will something bill from it, or must it be auditable?
      ├─ Yes ───────────────────────►  record/4         (one fact per id)
      └─ No  ───────────────────────►  track/4 with history

Separately, and at any point:
Do you have to refuse the work at a cap?
└─ Yes ──────────────────────────────►  with_quota/4    (atomic on one node)

Five things a reader would reasonably assume, that are not true

This programme found each of these the hard way. They are here because a mental model that omits them is marketing.

1. Buffered loss is not one flush interval. It is everything not yet in an acknowledged flush batch. While the database is unreachable the pending set grows without bound, and a hard crash loses all of it. If the number must survive, it is a record/4 fact, not a track/4 counter. (Guarantees G6.)

2. A hard cap can be exceeded in a cluster. Enforcement is against the local view. Nodes exchange deltas every :broadcast_interval and re-base on the persisted total every :flush_interval, so a burst across N nodes can overshoot by what the other nodes admitted between announcements. This is a design decision, not a defect: the alternative is a synchronous round trip on the hot path. (Guarantees G4, Clustering.)

3. A killed caller leaks its reservation. Process.exit(pid, :kill) inside a with_quota/4 callback cannot run the release, so the reserved units keep occupying the cap on that node until the period ends or the Store's ETS table is rebuilt. No monitor is added on purpose: releasing on a monitor would release work that is still running. (Guarantees G5.)

4. Credit lots are not retroactive. A wallet created by this release is born on the lot engine and gets the allocation trail, the documented spend order, debt and expired. A wallet that predates the release keeps the legacy balance until you run the migration, so one installation can hold two kinds of wallet and answer the same API differently for two tenants. The nine differences are tabulated in Credits under "Which wallets are on lots", and the migration is Upgrading to lots.

5. A wallet in debt is not simply negative. When a settle exceeds its hold the balance can go below zero, and that debt is recorded rather than hidden. In that state the wallet reports zero spendable rather than a negative number, and a further hold or debit is refused with {:error, :debt_outstanding} until the debt is cleared. A dashboard that renders available without checking debt will show a customer something that is not what they can spend. (Credits under "Debt".)

What the free core needs from you, and what it does not

It needs a Postgres repo, a Phoenix.PubSub server, and a plans module. That is all. Oban, LiveView, LiveDashboard, telemetry_metrics, OpenTelemetry, Plug and Igniter are optional, and "optional" here means a CI lane builds and tests this library with them removed rather than meaning nobody checked. See Support policy section 5.

It makes no outbound network request of its own, reports nothing to us, and stores only what you hand it: tenant keys, feature atoms, integer quantities and your own metadata. See SECURITY.md.

Where to go next

  • Getting started: install and the first counter.
  • Guarantees: the contract, one row per guarantee, each with what voids it and the test that proves it.
  • Correctness: the same ground per invariant, with the prerequisites, the known limits and the evidence.
  • Concepts: tenant, feature, period, plan, and the two shapes money takes.
  • Architecture: which process writes which table, and where the seams are.