Skip to main content

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.new policy 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.new with a plain message.
  • A failing policy function is logged and handled safely: a cost function falls back to overhead_cost, and an enabled rule counts as off. Engine logs go to console.error in 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.