Scheduler and tiers
Workers share themselves between tenants by weighted fair share over service tiers. You choose the tiers, their names and numbers; Paprika makes sure every tenant gets its share and none can crowd out the rest.
Tiers are set in PAPRIKA_TIERS, the same for the API and every worker, as JSON. The first tier is the default:
PAPRIKA_TIERS={"standard": {"weight": 1}, "priority": {"weight": 4, "maxRunning": 20, "ratePerMinute": 600}}| Setting | Means |
|---|---|
weight |
Its share of the workers: with weights 4 and 1, work starts 4 to 1 when both tiers are waiting |
maxRunning |
The most of one tenant’s work running at once, across every worker |
ratePerMinute |
The most synchronous renders and job and batch submissions one tenant may make a minute |
Unset, there is one tier, standard, with no limits. A malformed value stops the process at start, saying why.
A tenant’s tier
Section titled “A tenant’s tier”Give a tenant its tier with the admin API (tenants); a tenant with none has the default:
curl -X PATCH -H "Authorization: Bearer $PAPRIKA_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"tier": "priority"}' "$PAPRIKA_URL/v1/admin/tenants/fabrikam"{ "id": "fabrikam", "name": "Fabrikam", "tier": "priority", "retentionDays": 90, "createdAt": "2026-11-10T15:51:56.007Z", "suspendedAt": null, "deletedAt": null}A tier that isn’t configured is refused:
curl -X PATCH -H "Authorization: Bearer $PAPRIKA_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"tier": "gold"}' "$PAPRIKA_URL/v1/admin/tenants/fabrikam"{ "code": "bad-request", "message": "Bad request: tier is one of standard, priority, or null", "requestId": "69788ef8"}A tier removed from PAPRIKA_TIERS later falls back to the default. Queued work takes its tenant’s tier when a worker claims it, so an upgrade speeds up work already waiting.
A key can have a tier of its own, overriding its tenant’s for the work it submits: urgent work, or one department of an organisation that is a single tenant. Only the admin API gives one:
curl -H "Authorization: Bearer $PAPRIKA_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"name": "urgent", "scopes": ["render"], "tier": "priority"}' "$PAPRIKA_URL/v1/admin/tenants/fabrikam/keys"How work is shared
Section titled “How work is shared”The work queued is grouped by tenant and tier. A free worker takes from the group with the lowest share used: its work running and started in the last minute, divided by its tier’s weight. Ties go to the group waiting longest. So shares hold however few workers there are, every tenant counts on its own, and a lower tier always gets its share, more slowly.
Within a group, single jobs go first: they are someone waiting. Then batches take turns, the batch with the fewest records running first, each batch’s records in order.
Synchronous renders don’t queue: the API renders them in its own threads (PAPRIKA_WORKERS), answering 503 busy with Retry-After when they are all taken.
Reserved capacity
Section titled “Reserved capacity”A worker given PAPRIKA_WORKER_TIERS claims only those tiers’ work, so they always have workers of their own. Run it as a second worker service beside the one serving every tier:
worker-priority: image: ${PAPRIKA_IMAGE:-paprika:local} command: worker environment: PORT: 4180 PAPRIKA_DATABASE_URL: postgres://… PAPRIKA_TIERS: ${PAPRIKA_TIERS:-} PAPRIKA_WORKER_TIERS: priority volumes: - data:/dataA PAPRIKA_WORKER_TIERS naming a tier that isn’t configured stops the worker at start.
Rate limits
Section titled “Rate limits”Past its tier’s ratePerMinute, a tenant’s renders and submissions are refused with 429 rate-limited, Retry-After (the seconds to the next minute) and limit. The count is kept in PostgreSQL, so every API counts alike.
What tenants see
Section titled “What tenants see”A tenant’s queued job says how many of its own single jobs are ahead of it (queue.ahead). Other tenants’ work is never counted or shown.
Measures
Section titled “Measures”Read each tier’s settings, its work queued and running now, and over the last minutes the work started (with its wait) and done (with its render time):
curl -H "Authorization: Bearer $PAPRIKA_ADMIN_TOKEN" "$PAPRIKA_URL/v1/admin/scheduler?minutes=60"{ "minutes": 60, "tiers": [ { "tier": "standard", "name": "standard", "weight": 1, "maxRunning": null, "ratePerMinute": null, "queued": 0, "running": 0, "started": 12, "done": 12, "waitSeconds": { "p50": 0.2, "p95": 1.4 }, "renderSeconds": { "p50": 0.3, "p95": 0.9 } }, { "tier": "priority", "name": "priority", "weight": 4, "maxRunning": 20, "ratePerMinute": 600, "queued": 0, "running": 0, "started": 40, "done": 40, "waitSeconds": { "p50": 0.1, "p95": 0.4 }, "renderSeconds": { "p50": 0.3, "p95": 0.8 } } ]}Each worker also logs every claim with its tier and how long the work waited.