Pool sizes
Set how many requests obleth sends to a model at once, as a fixed number or one that follows the replicas its backend has ready.
5 min readUpdated for v2.4.0Edit on GitHub
OBLETH_DEFAULT_MODEL_MAX_IN_FLIGHT)discovered mode, from endpoints or a Kubernetes ServiceSet a model's pool
Every model has its own fairshare pool: the number of requests obleth has in flight to it at once. Requests beyond that wait in the model's queue, and tenant and key weights decide who goes next. See Fairshare engine.
On a model's page, under Capacity, set Max slots to about the number of requests the model's backend serves at once without queuing, such as the sum of your servers' maximum concurrent sequences. Blank uses OBLETH_DEFAULT_MODEL_MAX_IN_FLIGHT (default 32). To measure the number instead of guessing, run Capacity auto-tune.
curl -X PUT http://localhost:9180/api/v1/models/$MODEL_ID/capacity \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_in_flight": 32}'
Send {"max_in_flight": null} to go back to the default. The pool size is cluster-wide: with several gateway replicas, they share the slots between them. Admission weight does not change a model's pool.
A steady queue (obleth_queue_depth) means the pool is smaller than the load.
The global ceiling
OBLETH_GLOBAL_MAX_IN_FLIGHT (default 4096) caps the requests in flight across every pool together. It guards the gateway process, not any one backend, so keep it above the sum of the pools you expect to be busy at once; if the configured pools add up to more, the gateway logs a warning at start-up. GET and PUT /api/v1/capacity read and set it live, on the gateway replica that answers, until that replica restarts.
Follow the backend
When an autoscaler adds replicas, or a node drains, a fixed number goes stale. In the discovered mode the pool follows the backend:
pool size = max(1, ceil(ready replicas × per-replica concurrency × headroom))
Each gateway replica re-reads the backend every interval and resizes the pool without cutting in-flight requests short.
- 1
Turn on discovery
Set
OBLETH_CAPACITY_DISCOVERY_ENABLED=true, orobleth.capacityDiscovery.enabled: truein Helm. For thekubernetessource, also list the namespaces it may read (see Turn on discovery). - 2
Pick the mode
On the model's page, under Capacity, choose Discovered, then a Capacity source:
endpointsorkubernetes. - 3
Say what one replica takes
Set Per-replica concurrency, the requests one replica serves at once (for example vLLM's
--max-num-seqs), and Headroom (1.0is exactly the ready capacity). Fallback slots is the pool used while discovery has no answer. Save.
The section then shows the live count, for example "Live: 4 ready × 32 per replica = 128".
curl -X PUT http://localhost:9180/api/v1/models/$MODEL_ID/capacity-mode \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"capacity_mode": "discovered",
"capacity_source": "kubernetes",
"capacity_service": "my-model-serve",
"capacity_namespace": "inference",
"per_replica_max_in_flight": 32,
"capacity_headroom": 1.0
}'
| Field | Default | Meaning |
|---|---|---|
capacity_mode | static | static, tuned (set by auto-tune) or discovered |
capacity_source | endpoints | endpoints or kubernetes |
per_replica_max_in_flight | none | 1 to 100000. Required for kubernetes, and for endpoints unless every endpoint sets its own |
capacity_headroom | 1.0 | Above 0, at most 10 |
capacity_service, capacity_namespace | none | kubernetes: the Service and its namespace |
max_in_flight | none | The fallback |
A write that can't be discovered as configured (no per-replica value, a namespace outside the list, no Service and no default) is refused with 400 and a message saying what to set.
Sources
endpoints counts the model's own enabled, healthy endpoints: all of them under load balancing and sticky sessions, only the one in use under failover. A model with no endpoints counts its API base as one replica while it is healthy. An endpoint's own Max in flight wins over the model's per-replica value, and the values are added up: endpoints at 8, 2 and unset, with a per-replica value of 4, give 14. It needs nothing outside obleth.
kubernetes counts the ready endpoints of a Service, from its EndpointSlices. An endpoint counts when it is ready, serving and not terminating; nothing else is read. Which pods count is up to the Service's selector: point the model at a Service that selects only the model-server pods that take requests, not a gateway in front of them.
Choosing the Service
With the kubernetes source, Capacity lists the Services it can see in the allowed namespaces, each with its ready count, so you pick one rather than type it. If the model's default Service exists, it reads "Using <service> in <namespace> · N ready", with Change. Use default clears the choice.
The default comes from OBLETH_CAPACITY_DEFAULT_SERVICE, a template with {upstream_model} and {model_name} placeholders. A model with no namespace is looked up in each allowed namespace in order. The list comes from GET /api/v1/capacity/services.
Turn on discovery
| Environment variable | Helm value | Default | Meaning |
|---|---|---|---|
OBLETH_CAPACITY_DISCOVERY_ENABLED | obleth.capacityDiscovery.enabled | false | Run the discovery loop. Off, a discovered model uses its fallback |
OBLETH_CAPACITY_DISCOVERY_INTERVAL_SECS | obleth.capacityDiscovery.intervalSecs | 15 | Seconds between reads |
OBLETH_CAPACITY_DISCOVERY_NAMESPACES | obleth.capacityDiscovery.namespaces | empty | Namespaces the kubernetes source may read. Empty leaves it unavailable |
OBLETH_CAPACITY_DEFAULT_SERVICE | obleth.capacityDiscovery.defaultService | empty | Service name template for models that name none |
obleth:
capacityDiscovery:
enabled: true
intervalSecs: 15
namespaces:
- inference
defaultService: "{upstream_model}"
The chart ships this as examples/values-capacity-discovery.yaml. Docker Compose passes the switch and the interval through from .env; the kubernetes source needs the gateway to run in a pod.
RBAC
With discovery on and at least one namespace listed, the chart renders a ServiceAccount for the gateway pods and, in each listed namespace, a Role granting only get, list and watch on endpointslices (discovery.k8s.io), with its RoleBinding. There is no ClusterRole and nothing on pods, Services or Secrets. The namespaces must exist, and whoever runs helm must be allowed to create Roles in them.
Fallback states
| State | Pool size used |
|---|---|
discovered | The value read on this pass |
stale | The last value read, kept because the source has no answer: no replica serving, the Service gone, or the API server unreachable |
fallback | Fallback slots, or the default pool size: nothing has been read yet, or the model can't be discovered as configured |
Each change of state is logged once. Gateway replicas can disagree for up to one interval after a change.
Observe it
GET /api/v1/capacity/discovery reports the answering replica's settings and, for each discovered model, its source, Service, ready replicas, per-replica value, derived and in-force pool size, in-flight counts, state and the reason for a stale or fallback state. The metric obleth_capacity_discovery_models{state} counts models by state. The Deployments page shows the same for watched Kubernetes models.
Autoscalers and headroom
An autoscaler that scales on utilization or running requests still sees saturation at 100% of ready capacity. One that scales on queue depth at the backend only sees a queue if requests wait there: give that model a headroom above 1, such as 1.25.
With several gateway replicas
The derived value is the model's cluster-wide pool size, like a typed one. With shared slots (the default) every gateway replica draws from the same slots, so a backend with 4 ready replicas at 32 each gets at most 128 requests however the load balancer spreads them. When the slots are split (shared slots off, or Redis unavailable), each of 2 gateway replicas admits 64. See Multiple gateway replicas.
