Model Load Balancing¶
Model load balancing is Stage 1 of AI Gateway routing: loxilb reads the requested model name from each request and dispatches it to the backend pool that serves that model. This page covers how model-name matching works, how to configure per-model pools, and how to verify and troubleshoot them.
Where this fits
AI Gateway routing runs in two sequential stages. This page is Stage 1 (which pool). Once a pool
is chosen, Stage 2 picks a specific endpoint within it using the sel algorithm — see
LLM Routing. Both stages run for every request.
Why standard load balancers can't route by model¶
A single AI endpoint often fronts several models — a large model for deep reasoning, a smaller one for fast queries, an embedding model for RAG. Each model lives on a different backend pool.
Standard load balancers operate at Layer 4 and never read the HTTP body, so they cannot extract the "model" field from an OpenAI-format request and cannot dispatch it to the right pool. loxilb, running in fullproxy mode (mode: 4), inspects the HTTP request, extracts the model name, and selects the matching pool — before any backend is touched. Clients keep using a single VIP and the standard OpenAI request format.
How model-name routing works¶
Each LB rule carries a model_name. Multiple rules on the same VIP:port can differ only in model_name, each pointing to a different set of endpoints. loxilb determines the target model for a request in this priority order:
X-ModelHTTP header (highest priority) — lets a client select a model without modifying the request body."model"field in the JSON body — the standard OpenAI-compatible format, extracted from the parsed HTTP body.- Wildcard pool (lowest priority) — a rule with
model_nameset to the empty string ("") catches any request whose model does not match a specific rule.
If neither a specific rule nor a wildcard rule matches, loxilb returns HTTP 503 model_unavailable.
flowchart TD
REQ(["Incoming request"])
EXT{"Extract model name<br/>1. X-Model header<br/>2. JSON body model field"}
P1["Pool: llama-70b"]
P2["Pool: mistral-7b"]
P3["Wildcard pool<br/>(model_name = "")"]
R503([503 model_unavailable])
REQ --> EXT
EXT -->|"llama-70b"| P1
EXT -->|"mistral-7b"| P2
EXT -->|"no specific match"| P3
EXT -->|"no match, no wildcard"| R503
style P1 fill:#c8e6c9,stroke:#388e3c
style P2 fill:#c8e6c9,stroke:#388e3c
style P3 fill:#fff9c4,stroke:#f9a825
style R503 fill:#ffcdd2,stroke:#e53935
The X-Model header takes precedence over the JSON body, so a client can override the body's model field per request. On keep-alive connections the header is evaluated per request — an empty or absent X-Model falls through to the JSON body, then to the wildcard.
Routing and authorization disagree when header and body differ
Pool routing is header-first, but the current model-authorization gate evaluates the JSON body model first. A request carrying two different model names can therefore be authorized for one model and routed to another. Reject conflicting header/body values at a trusted upstream proxy, or require clients to send exactly one model source, until the implementation uses one canonical model identity for both decisions.
HTTP/2 feature boundary
The current HTTP/2 backend path passes an empty model into endpoint lookup and falls back to
round-robin for selector 9. It does not integrate model-aware pools, selector 10, P/D, or
KV-exact routing. Use backend_protocol: http1 for these features; treat HTTP/2 parity as a
development and release gate.
Prerequisites¶
FullProxy mode required
Model-name routing requires mode: 4 (FullProxy). L4 modes operate at the connection level and
cannot inspect the HTTP body to read the model field.
| Requirement | Details |
|---|---|
mode: 4 (FullProxy) |
Enables L7 HTTP body inspection — required to read the model field from each request |
backend_protocol |
Set to match your inference servers (http1 default, or http2 / both) |
REST API on port 11111 |
LB rules are created via POST /netlox/v1/config/loadbalancer |
Configuration¶
The example creates three rules on the same listener, VIP 10.10.10.254:2020: one per
named model plus a wildcard catch-all. The VIP, port, protocol, host, and path fields must
match across the rules; model_name is the part of the L7 key that separates their backend
pools. A wildcard on another port cannot catch an unmatched request sent to port 2020.
Each rule uses mode: 4 and sel: 0 (round-robin, the default), and includes the
host / path_prefix / path_match_mode fields so the L7 lookup key is well-formed.
Protect the management API
The curl examples use plain HTTP for an isolated lab. On a shared or production network,
use an authenticated, TLS-protected management endpoint. Read the authorization header from
a permission-restricted file; never put bearer values in a URL, command argument, or document.
| Port | model_name |
Backend |
|---|---|---|
| 2020 | llama-70b |
192.0.2.1:8080 |
| 2020 | mistral-7b |
198.51.100.1:8080 |
| 2020 | "" (wildcard) |
203.0.113.1:8080 |
Rule 1 — llama-70b pool¶
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 2020,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "10.10.10.254",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "llama-70b",
"inactiveTimeOut": 30
},
"endpoints": [
{"endpointIP": "192.0.2.1", "targetPort": 8080, "weight": 1}
]
}'
Rule 2 — mistral-7b pool¶
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 2020,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "10.10.10.254",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "mistral-7b",
"inactiveTimeOut": 30
},
"endpoints": [
{"endpointIP": "198.51.100.1", "targetPort": 8080, "weight": 1}
]
}'
Rule 3 — wildcard fallback¶
An empty model_name ("") makes this rule the catch-all for any model that does not match a specific rule.
curl -s -X POST http://10.10.10.254:11111/netlox/v1/config/loadbalancer \
-H "Content-Type: application/json" \
-d '{
"serviceArguments": {
"externalIP": "10.10.10.254",
"port": 2020,
"protocol": "tcp",
"sel": 0,
"mode": 4,
"host": "10.10.10.254",
"path_prefix": "/",
"path_match_mode": "prefix",
"model_name": "",
"inactiveTimeOut": 30
},
"endpoints": [
{"endpointIP": "203.0.113.1", "targetPort": 8080, "weight": 1}
]
}'
Per-pool selection algorithm
sel can differ per pool. sel: 0 (round-robin) is the default and is fine for even backends.
For cache locality within a plain pool, use sel: 8 (CHWBL). Selector 9 provides
prefix/session modulo placement on a plain pool; its P/D capacity-aware activation is currently
release-blocked. See LLM Routing.
Verify¶
List every rule and confirm each shows the expected model_name, mode: 4, and sel:
Then exercise each pool end-to-end:
# JSON body model field -> mistral-7b pool
curl -s http://10.10.10.254:2020/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"mistral-7b","messages":[{"role":"user","content":"hi"}]}'
# Negative routing test: the header selects llama-70b even though the body says mistral-7b.
# A real backend may reject the mismatched body model after routing.
curl -s http://10.10.10.254:2020/v1/chat/completions \
-H "X-Model: llama-70b" \
-H "Content-Type: application/json" \
-d '{"model":"mistral-7b","messages":[{"role":"user","content":"hi"}]}'
# No model -> wildcard pool
curl -s http://10.10.10.254:2020/
# With the wildcard rule installed, an unknown model reaches the wildcard pool
curl -s -o /dev/null -w "%{http_code}\n" http://10.10.10.254:2020/ \
-H "X-Model: unknown-xyz"
To test the 503 model_unavailable path, use a listener on which you intentionally did not
create a wildcard rule. Do not expect 503 from the three-rule listener above: its wildcard
is designed to accept the request.
Model matching is case-sensitive: MISTRAL-7B does not match a rule for mistral-7b.
Cleanup¶
Delete a model-specific rule with the same complete key used to create it. model_name is part of
that key; leaving it out matches only a model-less rule and can leave the intended pool active.
List the remaining rules after each deletion. For repeatable operations, assign a unique name at
creation and verify the selected rule before deleting it by name.
Troubleshooting¶
Requests routed to the wrong pool
- Check that
model_namein each rule matches exactly what clients send in the"model"field (case-sensitive). - Remember the
X-Modelheader takes precedence over the JSON body"model"field.
503 model_unavailable
- No specific rule matched and there is no wildcard rule (
model_name: "") for that VIP:port. Add a wildcard rule or correct the model name the client sends.
All traffic falls through to the wildcard
- Confirm
mode: 4and a matchingbackend_protocolare set — without fullproxy the HTTP body is never parsed. - Confirm the client sends the model in the JSON
"model"field or theX-Modelheader. - On keep-alive connections, confirm every request carries the intended
X-Modelheader — it is evaluated per request.
Model not found (404 from the backend)
- The pool was selected correctly but the backend inference server is not serving that model. Confirm the backend serves the expected model and that
targetPortmatches its serving port.
Cleanup returns 404 or leaves the model pool active
- Repeat the rule's exact
host,path_prefix,path_match_mode, andmodel_name. Omittingmodel_namedoes not select every model; it selects only the model-less key.
Uneven load within a pool
- Check endpoint health — unhealthy endpoints are excluded from selection.
- For a plain pool, use round-robin for independent requests or CHWBL for bounded prefix affinity.
Pushed worker metrics do not make plain-pool
sel: 9least-loaded; see vLLM Integration.
Next steps¶
- LLM Routing — Stage 2: endpoint selection within the pool chosen here.
- KV-Cache Routing — cache-aware routing for conversational workloads within a pool.
- vLLM Integration — vLLM parity and the selector-9 metrics boundary.
- Configuration Reference — every
serviceArgumentsfield and default.