Skip to content

Cap jobs running at once

Say exports are heavy, and you want each customer workspace to run at most 3 at once. The 4th should wait or be refused, and a crashed export mustn’t keep its slot forever.

import { createClient } from "@limitry/sdk";
const limitry = createClient();
await limitry.limits.create({
name: "Exports at once",
subjectKind: "workspace",
action: "export",
amount: 3,
window: "concurrent",
});

hold takes a slot before the job starts and gives it back when the job ends, whether it succeeds or throws:

import { LimitExceeded } from "@limitry/sdk";
try {
await limitry.hold(
{
subject: { kind: "workspace", id: workspaceId },
action: "export",
ttlSeconds: 600,
},
async (held) => {
for (const chunk of chunks) {
await exportChunk(chunk);
await held.extend(); // still running: keep the slot for 300 s from now
}
},
);
} catch (err) {
if (err instanceof LimitExceeded) {
// All 3 slots are taken. Try again in err.retryAfterSeconds.
return requeue(job, err.retryAfterSeconds);
}
throw err;
}

A slot is a reservation, and it lasts ttlSeconds. A job that’s still running extends it as it goes. A worker that crashes stops extending, and the slot frees itself within ttlSeconds. Pick a ttlSeconds comfortably longer than the gap between your extend calls. An extend() with no argument keeps the slot for 300 seconds from the call; pass a number of seconds to keep it longer.

A concurrency limit doesn’t count use. To also cap exports per day, add a second limit on the same action with the window day. The hold above then has to fit both.

Related: Rolling windows and concurrency, Reservations.