Skip to content

Budgets and limits

What your wisp may do is told, not discovered: ready carries its budget. A call past a budget does nothing and answers limited, with wait, the milliseconds before the same call can succeed. A wisp that paces itself from budget rarely sees one.

"budget": {"calls":100,"burst":500,"serverCalls":50,"channelLines":5,"edits":4}
Key Budget Counted
calls, burst calls on the line: 100 a second, in bursts of up to 500 for each line on its own
serverCalls calls about one server: 50 a second for your whole wisp, every line together
channelLines lines into one channel: 5 a second for your whole wisp
edits edits of one line, live frames included: 4 a second for your whole wisp

These are where Woosh starts, and they may change: read them from ready every time, and never write them into your code. A line’s calls refill steadily and save up to burst. serverCalls, channelLines and edits each count by the second, with no saving up. A call is checked against every one of those three it spends before any is counted, so a call limited there spends none of them. It has still spent one of the line’s calls, so a retry counts there again.

What each call spends:

Call Spends, besides the line’s calls
send serverCalls, channelLines
respond, a line nothing more
respond with edit the card’s edits only
respond with private nothing more
edit serverCalls, the line’s edits
card.live serverCalls, the line’s edits (a private card: its edits only). Past it, limited, and nobody saw the frame
delete, react, roles, kick, ban serverCalls
history, members, member, tally, draw serverCalls
activity one call every 15 seconds, the same words included; sooner answers limited with the wait

An answer spends neither serverCalls nor channelLines: the person paid for the run with their own budget, and a run has one answer, so answers come no faster than people ask.

And the people: each person may run commands and press wisps’ cards 3 times a second, across every wisp. Past that, their app tells them, and your wisp hears nothing.

{"r":42,"e":"limited","w":"Past the line's calls a second","wait":8}
{"r":13,"e":"limited","w":"Past the channel's lines or the server's calls a second","wait":200}

Nothing was done. w is for your log, and changes; read e and wait. Send the same call again after wait (a send under the same sendId, so it can never post twice).

When one call comes back limited, the second it hit is full, and every call in flight behind it into the same server or channel will be limited too. Retrying each after its own wait sends them all again at the same moment, into the next second, and most bounce again. Instead, keep the budgets yourself and wait before you call. serverCalls, channelLines and edits each allow so many calls in each one-second window, so a note of when each call goes is all it takes; the line’s calls is a bucket that refills:

const SECOND = 1100; // one second as Woosh counts it, and a little more for the trip there
/** serverCalls, channelLines or edits: at most `limit` calls in each one-second window. */
class Window {
constructor(limit) {
this.limit = limit;
this.times = []; // when its calls go, from a second ago on
}
/** The earliest moment from `at` on with room for one more call. */
free(at) {
const crowd = (t) => this.times.filter((x) => x > t - SECOND && x < t + SECOND).length;
const tries = [at, ...this.times.map((x) => x + SECOND).filter((t) => t > at)].sort((a, b) => a - b);
return tries.find((t) => crowd(t) < this.limit);
}
book(at) {
const now = performance.now();
this.times = this.times.filter((x) => x > now - SECOND).concat(at);
}
}
/** A line's calls: `rate` a second, up to `burst` saved. */
class Bucket {
constructor(rate, burst) {
Object.assign(this, { rate, burst, tokens: burst, at: performance.now() });
}
/** The earliest moment from `at` on with a call to spend. */
free(at) {
const tokens = Math.min(this.burst, this.tokens + ((at - this.at) / 1000) * this.rate);
return tokens >= 1 ? at : at + ((1 - tokens) / this.rate) * 1000;
}
book() {
const now = performance.now();
this.tokens = Math.min(this.burst, this.tokens + ((now - this.at) / 1000) * this.rate) - 1;
this.at = now;
}
}
let budget; // from ready
let line; // new Bucket(budget.calls, budget.burst), at each ready
const windows = new Map(); // "server:<id>", "channel:<id>", "edits:<id>" -> its Window
function kept(key, limit) {
if (!windows.has(key)) windows.set(key, new Window(limit));
return windows.get(key);
}
const inServer = (id) => kept(`server:${id}`, budget.serverCalls);
const inChannel = (id) => kept(`channel:${id}`, budget.channelLines);
const editsOf = (id) => kept(`edits:${id}`, budget.edits);
/** Books one call on the line and in each budget it spends; the milliseconds to wait before sending it. */
function slot(...spends) {
const now = performance.now();
const all = [line, ...spends];
let at = now;
while (all.some((b) => b.free(at) > at)) at = Math.max(...all.map((b) => b.free(at)));
for (const b of all) b.book(at);
return at - now;
}
const sleep = (ms) => new Promise((go) => setTimeout(go, ms));
// a line: await sleep(slot(inServer(d.server), inChannel(d.channel))); then call("send", …)
// an edit, or a frame: await sleep(slot(inServer(d.server), editsOf(message)));
// a read, a reaction: await sleep(slot(inServer(d.server)));
// an answer to a run: await sleep(slot());

A wisp that waits as its windows say stays inside every budget, and a call that waits for one channel holds back no call into another. The budgets other than calls are your whole wisp’s, so share one set of windows between the lines of one process. Keep the limited retry from The line as the fallback, for the call your windows could not foresee: one from your other process during a hand-over, say.

A card that changes faster than 4 times a second does not need every change shown. Keep the newest change for each line, and send it when the line’s edits allow; anything in between is dropped by you, before it costs anything:

const newest = new Map(); // line id -> the latest card waiting
function show(server, message, card) {
const scheduled = newest.has(message);
newest.set(message, card);
if (scheduled) return; // the frame on its way takes this card
setTimeout(() => {
const latest = newest.get(message);
newest.delete(message);
call("card.live", { message, card: latest }); // answered like any call
}, slot(inServer(server), editsOf(message)));
}

Better still, let the app do the moving: an animated picture costs no edits at all, and a time mark counts down by itself.

What Limit
Calls on a line 100 a second, bursts of 500 (budget)
Calls about one server 50 a second, for your whole wisp (budget)
Lines into one channel 5 a second (budget)
Edits and frames of one line 4 a second (budget)
A person’s runs and presses 3 a second, across every wisp
Your wisp’s words one activity call every 15 seconds; they last two minutes
Calls in flight on a line 32
A call’s answer within 10 seconds, or failed
Answering a run or a press once, within 15 seconds
A message you send one frame, 64 KB
The hello within 10 seconds of the socket opening
Silence before a line is dead 30 seconds (silence)
The replay window 10 minutes (replay: 600)
The group’s log its last 16,384 events, kept 10 minutes past the newest
A replay 4 MB; past it, the group is in g
A line’s unread backlog 8 MB, then it is dropped
A line’s age asked to move at 5 to 6 days
A move moved within 2 minutes, or the old line is closed with 1012
Lines open at once twice your slices
Slices 1 to 64
Wisps one person makes 25
A wisp’s description 400 characters
The token an hour; 120 token requests a minute from one address
Commands 50 a wisp; 10 options a command; 1 to 25 choices
A command or option’s name 1 to 32 lowercase letters, digits and -, starting with a letter or a digit
A text option 1 to 1,000 characters
The text of a posted line 4,000 characters
A card 40 parts, 4,000 characters of text, 8 KB
An action’s name, a menu’s value, a key 1 to 32 of a to z, 0 to 9, _ and -, starting with a letter or a digit
An action’s or a cell’s value 1 to 100 bytes
A menu 1 to 25 options
A channel’s history 100 lines a page
A server’s members 1,000 a page
A tally’s list 1,000 a page
A draw 1 to 100 people
Pictures PNG or APNG, JPEG, WebP or GIF; 2 MB; two uploads a second
Servers before review, for a wisp that reads messages 100

© 2026 Woosh AS. All rights reserved.