Skip to main content

How Limitr works

Limitr has two parts:

  • The policy: one document that says what each plan includes, how usage is counted, what it costs you, and what you charge.
  • The engine: a small WebAssembly runtime inside your app that loads the policy, keeps each customer's state, and answers one question before anything that costs money: is this allowed, and what is it worth?

The policy​

PieceWhat it isEx.
CreditA unit you count, with what you charge and what it costs you.An AI token: $20 per million charged, $8 per million cost.
PlanA named set of entitlements and topups.Free, Pro, Enterprise.
EntitlementSomething a plan allows. No limit: a feature flag. With a limit: metered in a credit.export_pdf, chat, seats.
LimitHow much of a credit an entitlement includes, its mode, and when it resets.2M tokens a month, soft.
TopupA credit pack on a plan.1M tokens for $15.
ExchangeConverts any credit into any other, through runes (US dollars by default). Currencies are credits too.1 AI credit is 1 cent; 1 euro is 1.14 USD.
NotificationA rule that watches events."Tell me when a customer has 10% left."

A policy is a Stof document, a superset of JSON, so it can also be written in JSON, YAML, or TOML. Stof adds units (1GB, 30days) and functions, for the parts of pricing that aren't just numbers: a cost that depends on the model, a rollout rule, or a rule of your own (Extend Limitr).

Customers​

A customer is anything you meter: a user, an org, a team, an agent. Each has your ID, a plan, a type, and refs to other customers (a user refs its org, and can be on the org's plan). The engine keeps each customer's state:

  • Meters: usage of each entitlement this period, and what it cost.
  • Overrides: per-customer limits, Ex. a bigger allowance for one deal.
  • Gives and takes: per-customer changes to which entitlements it has.
  • Grants: credit balances from topups.
  • Caps: spend ceilings, Ex. $50 a month across everything.

A call​

Your app asks before doing the work:

if (await limitr.allow(customerId, 'chat', tokens)) {
// call the model
}

The engine decides in this order, and nothing changes until every step passes:

  1. Does the customer have the entitlement? Its plan (or a give), not taken away, and on by its feature rule, if it has one. A feature flag stops here.
  2. New period? If the limit resets and a period ended, the meter starts over.
  3. Spend caps. Every cap that applies has room.
  4. Grants cover usage past the limit first.
  5. Rate. A governor, if there is one, has tokens left.
  6. The limit, by its mode:
ModePast the limit
hardDenied.
softAllowed, and the part past the limit is billed as overage.
observeAllowed, and only recorded.
  1. Commit. The meter, its cost, grants, and caps move, and events are sent.

check makes the same decision without changing anything. For calls whose size you only know afterwards, like an LLM call, reserve and settle hold room first and record the real usage after.

Try the three modes: each entitlement allows 2 exports.

policy: {
    credits: {
        export: { label: 'Export', price: { amount: 0.50 } }
    }
    plans: {
        pro: {
            default: true
            entitlements: {
                exports_hard: { limit: { credit: 'export', mode: 'hard', value: 2 } }
                exports_soft: { limit: { credit: 'export', mode: 'soft', value: 2 } }
                exports_observe: { limit: { credit: 'export', mode: 'observe', value: 2 } }
            }
        }
    }
}
modes.stof
#[main]
fn main() {
    self.policy.create_customer('ada');
    for (const entitlement in ['exports_hard', 'exports_soft', 'exports_observe']) {
        pln(entitlement);
        for (const i in 3) pln('  export', i + 1, '->', self.policy.allow('ada', entitlement, 1));
    }
}

#[meter-limit]
fn denied(event: obj) { pln('  meter-limit: denied at', event.meter.value, 'of', event.meter.limit); }

#[meter-overage]
fn overage(event: obj) { pln('  meter-overage: bill', event.overage_price); }
Output

Events​

Every change is reported as an event, to your app's handlers and to the policy's notifications: meter-changed for usage, meter-overage for usage to bill (with overage_price), meter-limit for a denial, cap-limit when a budget stops a call, customer-set when a customer changes, and more. Meter events carry what the call is worth (meter.price_diff) and what it cost you (meter.overhead_diff), so margin is known per call.

Limitr doesn't take payments. It decides what's billable and tells you; your app (or Limitr Cloud) records it and your payment provider charges it. See Bill from events.

Where it runs​

The engine runs in your process: Node, Bun, Deno, or the browser, with no network in the way. Policy functions run in a sandbox, with no network, files, or environment unless your app allows them, and a time limit on every call. See Runtime and sandbox.

Customer state lives in the engine's memory. Self-hosted, you save it in your database. With Limitr Cloud, it's stored and synced across every server for you.