Runtime and sandbox
The Limitr engine is a Stof document compiled to WebAssembly: the engine's types and functions plus your policy and customer state, in one document, inside your process.
Where it runs
Node, Bun, Deno, and the browser, from the same package. There's no service to run, and no network call happens
during allow, check, or any other engine call.
The sandbox
Functions in a policy (cost functions, enabled rules, notifications) run in Stof's sandbox:
- No network unless your app allows it. A
Limitr.newpolicy has none by default. A Cloud policy can only reach Limitr Cloud (api.limitr.dev). - No files and no environment variables.
- Only the functions you register. Your app can add host functions the policy may call (see Call your app from a policy).
Allow HTTP from the policy for listed hosts, or for any host:
limitr.doc.allowHttp(['api.example.com']);
limitr.doc.allowHttp(); // any host
With Cloud, use Limitr.cloud({ httpHosts: ['api.limitr.dev', 'api.example.com'] }) (true for any host, false
for none).
The sandbox is what makes it safe for a policy to carry logic at all, including a policy someone else (or a model) edited.
Time limit
Every policy call has a time limit, 5 seconds by default. A call that runs longer fails instead of blocking every call after it, Ex. a cost function stuck in a loop. You'll only see this when a policy function goes wrong.
limitr.setMaxExecutionTime(2000); // 2 seconds
limitr.setMaxExecutionTime(null); // no limit
With Cloud, pass maxExecutionTime to Limitr.cloud.
Memory
The engine document can live for months in a long-running server, and Stof has no garbage collector, so the engine creates nothing per call except state that belongs there. Memory grows with:
- customers and their meters, grants, caps, and overrides,
- open reservation holds (they expire),
- estimate segments (one per entitlement and segment value; keep segment values few).
It doesn't grow with calls. Code you add to a policy has to keep it that way: see Write policies in Stof.
Concurrency
The SDK runs one engine call at a time, through a queue, so each call is atomic. A sequence of calls isn't: use reservations when concurrent requests mustn't overspend.
Each process has its own engine and its own copy of customer state. Several servers sharing customers need a shared source of state: save and load customers yourself, or use Limitr Cloud, which syncs every engine.
Errors
- Calls don't throw for unknown customers or entitlements; they're denied.
- An invalid policy throws from
Limitr.newwith a plain message. - A failing policy function is logged and handled safely: a cost function falls back to
overhead_cost, and anenabledrule counts as off. Engine logs go toconsole.errorin JavaScript.
Reading the document
limitr.doc is the underlying Stof document (StofDoc). Read and export anything in it:
limitr.doc.get('policy.plans.pro.entitlements.chat.limit.value'); // 2000000
limitr.doc.stringify('json', 'policy.plans'); // or yaml, toml, stof
Ask a question the API doesn't have a method for, with a Stof function that runs on the policy and leaves nothing behind:
const plans = await limitr.doc.callSource(`
fn plans_with(entitlement: str) -> list {
const names = [];
for (const (name, plan) in self.plans.fields()) {
if (plan.entitlements.contains(entitlement)) names.push_back(name);
}
names
}
`, ['sso'], 'policy');
Change customers, meters, and plans through Limitr's methods, not the document: they keep meters consistent and send the events your app and Cloud listen for. See the Stof tutorial chapter.
Exporting the policy
limitr.doc.stringify('json', 'policy') exports the policy with its customers as JSON (or YAML, TOML, or Stof), and
difference(otherLimitr) diffs two policies.