Save customer state
The engine keeps every customer in memory, which is why calls are fast. Self-hosted, saving that state is your job: a restart loses anything you didn't save. (With Limitr Cloud, Cloud stores and syncs it.)
A customer's record holds all of its state: meters, holds, estimates, overrides, gives and takes, grants, caps, and metadata. Save the record, load it back, and the customer is exactly where it was.
import { Limitr } from '@formata/limitr';
const policy = `
policy: {
credits: { request: { label: 'Request' } }
plans: {
free: {
default: true
entitlements: {
api: { limit: { credit: 'request', value: 100, resets: true } }
}
}
}
}`;
// Stands in for your database: customer ID -> record.
const db = new Map<string, unknown>();
async function startEngine(): Promise<{ limitr: Limitr; flush: () => Promise<number> }> {
const limitr = await Limitr.new(policy);
// Remember which customers changed, and write them in batches instead of on every call.
const dirty = new Set<string>();
limitr.addHandler('persist', (name, value) => {
const event = JSON.parse(value as string);
if (name === 'customer-set') dirty.add(event.id);
else if (event.customer?.id) dirty.add(event.customer.id);
});
const flush = async () => {
const ids = [...dirty];
dirty.clear();
for (const id of ids) db.set(id, await limitr.customer(id));
return ids.length;
};
return { limitr, flush };
}
// Load a customer from the database the first time this server sees it.
async function loadCustomer(limitr: Limitr, id: string) {
const saved = db.get(id);
if (saved) await limitr.ensureSetCustomer(saved as Record<string, unknown>, false);
else await limitr.ensureCustomer(id);
}
let { limitr, flush } = await startEngine();
for (const user of ['ada', 'bo', 'ada', 'ada']) {
await loadCustomer(limitr, user);
await limitr.allow(user, 'api', 1);
}
// In production, flush every few seconds and on shutdown.
console.log('wrote', await flush(), 'customers');
// A restart, or another server.
({ limitr, flush } = await startEngine());
await loadCustomer(limitr, 'ada');
console.log('ada after restart:', await limitr.value('ada', 'api'));
It prints:
wrote 2 customers
ada after restart: 3
When to save
customer-setfires when a customer is created or replaced, or its plan, overrides, gives and takes, alt IDs, caps, or included topups change. Its payload is the customer.- Meter events carry the customer whose meter moved (
event.customer). They don't also sendcustomer-set. - Batch it. Usage events can fire on every request. Mark the customer dirty, and write dirty customers every few seconds, as above, plus on shutdown.
- Everything at once:
limitr.customers()returns every customer, Ex. for a nightly snapshot.
When to load
| Pattern | How |
|---|---|
| At startup | loadCustomers(records): an array, or an object keyed by ID. Simple, and fine for thousands of customers. |
| On first use | ensureSetCustomer(record) the first time a server sees a customer, as above. Memory stays proportional to active customers. |
| One customer changed elsewhere | setCustomer(record) replaces it. |
Loading sends customer-set for each customer. Pass false as the event argument to setCustomer and
ensureSetCustomer so loading doesn't mark customers dirty again, and add your save handler after loadCustomers.
More than one server
Each server has its own engine and its own copy of each customer. Two servers metering the same customer each see only their own calls, until they reload. Options:
- Route a customer to one server (sticky sessions by customer or org), and save and load as above.
- Reload before deciding for customers that can be on several servers, and save right after. Simple, but the database is back in the request path.
- Limitr Cloud keeps every engine in sync over a background connection, with no network call in the request path, and denies calls while a server is disconnected so state can't drift.
What's in a record
The record is plain JSON (LimitrCustomer), so store it as a JSON column or document. Don't edit meters by hand:
change customers through Limitr's methods, which keep meters consistent and send events. See
Customers.