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.

Terminal window
curl -X POST https://api.limitry.com/v1/limits \
-H "Authorization: Bearer $LIMITRY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"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 limits.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 another 600 s
}
},
);
} 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.

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.