OBLETHdocs

How fairshare works

What the gateway does when more requests arrive than a model's backend can serve, and how to read the Fairshare page.

5 min readUpdated for v2.4.0Edit on GitHub

Shared unitOne pool of slots per model
Next slot goes toGroup, then tenant, then key, by weight
When a pool is fullRequests queue for up to 60 s, then get a 503
Across replicasLimits hold for the whole fleet

Each model has its own pool

A pool is the number of requests the gateway lets reach a model's backend at once. Its size is the model's max in flight, or OBLETH_DEFAULT_MODEL_MAX_IN_FLIGHT (default 32) when the model has none. A model in the discovered capacity mode sizes its pool from the backend's ready replicas instead; see Pool sizes.

Pools are independent. A full pool never delays a model with free slots, and a tenant's use of one model costs it no priority on another. OBLETH_GLOBAL_MAX_IN_FLIGHT (default 4096) is a safety ceiling across all pools together; keep it above the sum of your pool sizes so the pools do the limiting.

When a pool is full, requests queue

A request that finds its model's pool full waits in that pool's queue instead of failing. When a running request finishes, the scheduler hands its slot to the waiter that is furthest below its fair share. The client sees a slower response, not an error.

A waiter whose client has disconnected is dropped from the queue and charged nothing. Requests already running are never cut short to make room.

Who gets the next slot

Slots are divided in three steps, each by weight:

  1. Groups. Each pool's slots are split between the active fairshare groups by group weight.
  2. Tenants. A group's share is split between its active tenants by tenant weight.
  3. Keys. A tenant's share is split between its keys by key weight, so one busy key cannot take all of its tenant's slots.

Only tenants that are running or waiting on the pool count. A share is a claim when the pool is contended, not a reservation: slots a quiet group or tenant leaves unused are lent to whoever is waiting, and are returned as those borrowed requests finish.

Fairness is measured in tokens, not requests. Each request is charged its estimated token count when it is admitted, so a tenant sending long prompts uses up its share faster than one sending short ones.

A tenant or key can also carry a max in flight: a hard cap on how many of its requests run at once in each model's pool. Requests over the cap wait like any other. Set weights and caps on the tenant's page and in the key's panel; see Tenants and keys. To change a weight during an incident, see Priority boost.

The scheduling algorithm, the scores it keeps and how shares are computed are in Fairshare engine.

Limits hold across replicas

With more than one gateway replica, every limit above (pool sizes, the global ceiling, tenant and key caps) holds for the fleet as a whole. A model with 20 slots behind three gateways admits 20 requests in total, on whichever gateways they reach. The slots are counted in Redis (OBLETH_FAIRSHARE_SHARED_SLOTS, on by default). With a single replica there is no Redis call on the request path.

If Redis stops answering, each gateway falls back to its share of each limit (the configured value divided by the number of live gateways, rounded up) and returns to shared slots when Redis recovers. It never admits without a limit.

Queued requests time out

A request that waits OBLETH_ADMISSION_TIMEOUT_SECS (default 60) without getting a slot leaves the queue with a 503 and a Retry-After header:

HTTP/1.1 503 Service Unavailable
Retry-After: 5
Content-Type: application/json

{"error":{"message":"timed out waiting for model capacity","type":"obleth_gateway_error"}}

Clients should wait and retry. The OpenAI and Anthropic SDKs already retry on 503 and respect Retry-After.

Queuing covers contention. A request is refused at once only when it crosses a limit you set:

StatusCause
429 token budget exceededThe tenant's token rate is used up. See Budgets and rate limits
403 tenant term budget exhaustedThe tenant's spend or token budget for the period is used up
403 api key term budget exhaustedThe key's own budget for the period is used up
403 model not permitted for tenantThe model is not on the tenant's model list
403The key is disabled, the tenant is suspended or archived, or the tenant is outside its access hours. The message says which
503 scheduler unavailableThe scheduler could not be reached; the gateway also raises an alert

Read the Fairshare page

Fairshare opens on every model pool, fullest first, with the number of requests running and waiting, the busiest pool, and each group's use against the share its weight guarantees.

The Fairshare page: running and waiting tiles, model pools sorted by how full they are with one pool full and 56 requests waiting, and a Groups panel with each group's use against its share

A pool is Busy above 80% and Full at 100%. A pool whose model has nowhere to send a request (no API base, no enabled endpoint, or a watched backend with no ready replica) reads No servers, and its slots are left out of the totals. Open a pool (or follow a Full item from the Overview) to see who holds its slots:

One model pool: every slot taken, three tenants each running their fair share with requests waiting, and the keys in the pool with their weights and caps

TermMeaning
Fair shareThe slots a tenant gets when every active tenant in the pool is busy: its group's share, split by tenant weight. Drawn as a tick on each bar
Above shareSlots a tenant holds beyond its fair share, borrowed while nobody else needed them
Next in lineA tenant waiting while below its fair share. These tenants get freed slots first, in the order shown
At shareA tenant with requests waiting that already holds its fair share
Borrowing slots others are not usingA group running above its share because other groups are idle

The (?) beside each tenant explains why it is waiting and who holds slots above their share; the one beside Full lists what you can do. Open a tenant to change its weight or cap it on that model, with a preview of who gives up slots. Groups & weights changes group weights, previewing every group's new share before you save.

When requests wait too long

A pool that queues most of the time is too small for the traffic it gets.

  • Raise the model's max in flight, if the backend can serve more at once. Capacity auto-tune finds the backend's limit.
  • Add backend replicas. Adding gateway replicas does not add backend capacity.
  • Check weights: one high-weight tenant can hold most of a busy pool.
  • Keep OBLETH_GLOBAL_MAX_IN_FLIGHT above the sum of your pool sizes. The gateway logs a warning at start when it is not.
Finding queued and timed-out requests in the ledger

Every request is recorded with an admission class (fast, queued or rejected) and queue_wait_ms. Admission timeouts are rejected rows with status 503; budget refusals are rejected rows with 429 or 403.

SELECT tenant_id, admission, count() AS requests, avg(queue_wait_ms) AS avg_wait_ms
FROM usage
WHERE ts_ms > toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000
GROUP BY tenant_id, admission
ORDER BY requests DESC

In the dashboard, the bar on each row of Request logs splits a request's time into waiting for a slot, time to first token, and the rest.