Routing and Model Management: llama-swap, LiteLLM, NGINX and Health Checks
By the end of this lesson you will be able to say, for any request arriving at your machine, which component decided it was allowed, which decided which model would answer, which decided whether that model was running, and which of those four layers to look at first when the answer is wrong. You will also be able to change the model behind a name without anything that uses the name noticing.
Part 9 built two of those layers and left the front door for later. This is later.
Three layers, three jobs, one address
Section titled “Three layers, three jobs, one address”The temptation, when a second person starts using your machine, is to add the missing piece to whatever you already have. The engine gets an API key. Then the engine gets a second port for the second model. Then something needs a certificate, so the certificate goes on the engine too. Six months of that produces a system where nothing can be changed because everything knows about everything.
The alternative is three small programs, each of which knows only about the one below it.
A request arriving from the network, on its way to a model
- NGINXIs this connection encrypted, is this client asking too often, and is this body a sensible size? Terminates TLS, applies the rate limit, publishes one port.transport and abuse
- LiteLLMWho is asking, may they have this model, have they used their allowance, and where should it go? Keys, budgets, aliases, retries, fallbacks, usage records.identity and policy
- llama-swapIs the right engine running for the model that was asked for? Starts it if not, stops something else if it must, unloads it when idle.memory as a schedule
- Enginesllama-server, vLLM, SGLang or an MLX server, each holding one model and a fixed number of slots
- Accelerator and memoryThe one physical resource all of the above are competing for
The value of the arrangement is not architectural elegance. It is that each layer fails
visibly and separately, and that you can replace any one of them without touching the
others. Swap llama-server for vLLM behind an alias and no client changes. Put a second
machine behind the router and no engine changes. Move the certificate from a private
authority to a public one and nothing below NGINX notices.
llama-swap: memory as a schedule
Section titled “llama-swap: memory as a schedule”llama-swap v255 · verified 2026-09-08 is a single Go binary whose entire job is to make sure the engine that can answer a request is the one that is running. Part 9 configured it. What Part 9 did not do is treat its timing keys as operational settings, which is what they are.
Four keys decide how a machine behaves under a day of mixed use. The example configuration
documents healthCheckTimeout as “number of seconds to wait for a model to be ready to
serve requests” and checkEndpoint as the “URL path to check if the server is ready”.
Those two together are how llama-swap knows a model has finished loading rather than
merely having been started, which matters because a large model on a slow disk can take
longer to become ready than any default patience allows.
The other two are about giving memory back. ttl is documented as “automatically unload
the model after ttl seconds”, and unloadTimeout as the “graceful timeout in seconds when
unloading a model (manual, API, or ttl expiry)”. A short ttl on a desktop you also use
for other things is correct. The same short ttl on a machine serving a household is a
recipe for everybody waiting through a model load several times a day, and it is the most
common cause of the complaint that the assistant is “sometimes slow for no reason”.
Groups decide what may coexist. The documentation describes groups as letting you “keep
some models loaded while others swap out”, with swap controlling “how members of this
group swap among themselves”, exclusive controlling “how this group affects other
groups”, and persistent meaning that “other groups cannot unload this group’s members”.
Part 9’s configuration puts Qwen3-8B and Qwen3-Coder-30B-A3B in one swapping, exclusive
group and Qwen3-Embedding-0.6B in a persistent one, which is the right shape for almost
every single-machine estate: the big models take turns, the small always-needed one stays.
Swapping a model without anybody waiting
Section titled “Swapping a model without anybody waiting”Model swapping is not free and the documentation does not pretend otherwise: when a request arrives for a model that is not loaded, llama-swap starts the right engine, and the caller waits for that. The README’s description is plain: if the wrong upstream server is running, it “will be replaced with the correct one”. Replacement takes as long as loading takes.
There is a way to make the upgrade case free, and it is worth knowing because it is the case that happens on purpose. The trick is to treat the model id as versioned and the alias as the thing that moves.
Rolling a new model in behind an unchanged name
- Add the new model under a new idA second entry in llama-swap.yaml, for example a dated id, pointing at the new file. Nothing refers to it yet, so nothing changes.
- Put it in a group that can coexist, if memory allowsOn a machine with room, the new model can load while the old one is still serving. On a machine without room, accept a gap and do this when nobody is asking.
- Warm it with one requestSend a request naming the new id directly. It loads, it answers, and now it is resident. This is the step that moves the waiting off your users and onto you.
- Compare it against the old oneYour Part 16 evaluation set, on both ids, before anything else points at it. A model that is worse on your own tasks is a model you should not have rolled.
- Move the aliasOne line in litellm-config.yaml: the alias local/chat now points at the new id. Clients keep sending the same name.
- Keep the old id for a weekBoth entries stay. If something regresses, you move one line back rather than re-downloading a model at speed under pressure.
- Remove it deliberatelyWhen the week has passed and nothing complained, delete the old entry and record the change in the changelog.
LiteLLM: who is asking, and how much may they have
Section titled “LiteLLM: who is asking, and how much may they have”LiteLLM 1.100.0 · verified 2026-09-08 is the layer that knows about people. Part 9 used it for aliases, retries and fallbacks. Three of its facilities become load-bearing the moment more than one thing is calling.
Keys per caller. The virtual keys documentation states the requirements plainly: a
Postgres database reachable through DATABASE_URL, and a master key that must start with
sk-. /key/generate takes a models list, metadata, and a duration, so a key can be
limited to the models one application needs and expire on its own. The master key is the
proxy admin key that creates other keys; applications should carry the keys it issues
rather than the key itself.
Budgets and rate limits. The same interface takes max_budget with a
budget_duration written as a period such as 30d, plus tpm_limit and rpm_limit for
tokens and requests per minute. These can be set on a key, on a team or on an internal
user, and the documentation notes that budgets require a database. On a home service the
money is not the point, because the marginal cost of your own machine is electricity. The
point is the shape of the limit: a runaway script that would otherwise occupy your
accelerator all night hits rpm_limit and stops, and the record of it hitting the limit is
in the usage table.
Routing across deployments. The configuration documentation shows several entries in
model_list sharing one model_name with different api_base values, which is how one
alias is served by more than one machine. routing_strategy selects between
simple-shuffle, least-busy, usage-based-routing and latency-based-routing. For a
pair of machines from Part 20 or Part 21, least-busy is the honest default: it sends work
to whichever is not currently occupied, which is exactly the property a two-machine home
cluster has that a single machine does not.
Health, which is three questions
Section titled “Health, which is three questions”The health documentation is unusually clear that these endpoints answer different things, and the difference matters because a monitor asking the wrong one gets a confidently wrong answer.
GET /health/liveliness means “the process is up. No dependencies are checked”. It needs
no authentication. Use it for a process supervisor: if this fails, restart the container.
GET /health/readiness means “the worker is ready to accept traffic”, and reports whether
the database is "connected", "disconnected" or "Not connected". Use it to decide
whether to send a request at all.
GET /health is different in kind. The documentation says it “runs a real test request
against every configured model, so it costs a few tokens per model”. On a swapping gateway
that means loading every model in turn. This is a useful thing for a human to ask once and
a destructive thing for a monitor to ask every fifteen seconds, which is why
background_health_checks: true with health_check_interval: 300 exists: the checks run on
a timer and the endpoint serves the cached answer.
Underneath, llama-server’s own /health is simpler and just as useful: the README
documents it as returning 503 while “the model is still being loaded” and 200 with
{"status": "ok"} when “the model is successfully loaded and the server is ready”. That is
precisely the signal llama-swap’s checkEndpoint is reading.
Fallbacks, and when they are wrong
Section titled “Fallbacks, and when they are wrong”The reliability documentation gives three kinds. fallbacks sends an alias to another one
when it fails outright. context_window_fallbacks sends it elsewhere specifically when the
context window was exceeded, which on a local estate is a genuinely good idea: a request
too long for the 8B model can go to the one with the longer context rather than failing.
content_policy_fallbacks covers refusals. Around them, num_retries retries within one
deployment, allowed_fails takes a deployment out of rotation after that many failures and
cooldown_time says for how long.
Part 9 made the design point and it is worth repeating in operational terms: a fallback converts a visible failure into a quiet degradation. For a person waiting, that is a good trade. For an overnight batch writing numbers into a results table, it is a corrupted result set that looks fine. Decide per alias, and write the decision down.
The front door
Section titled “The front door”Part 7 put Caddy in front of a chat front-end, and Caddy was the right choice there: its
documentation describes generating “its own certificate authority (CA)” to “serve
non-public sites over HTTPS”, installing the root into the system trust store on first use,
and offering caddy trust to retry that “as a privileged user”. For a name that exists only
inside your house, that is the shortest path to working TLS and Part 7’s procedure still
stands.
This part uses NGINX for the API front door, for one reason: rate limiting. The
limit_req module gives a documented, well-understood control that an API endpoint needs
and a chat front-end mostly does not. Its two directives are
limit_req_zone key zone=name:size rate=rate; in the http context, which the
documentation notes will hold “about 16,000” states per megabyte for the
$binary_remote_addr key, and limit_req zone=name [burst=number] [nodelay]; where
requests within the burst are queued and those beyond it are refused, by default with
status 503.
If you would rather keep one reverse proxy across the whole house, keep Caddy and put the
rate limiting somewhere else, such as rpm_limit on every issued key. If you would rather
have the limit at the edge, where it costs the machine nothing to enforce, this is the file:
RunnableAll tracks
# Purpose: put one hardened front door in front of the Part 9 gateway - TLS for the whole# conversation, a per-client request rate limit, a body-size ceiling, and# streaming that is not ruined by response buffering# Platform: all (this is an NGINX server block; on Linux it goes in# /etc/nginx/conf.d/, on macOS under the Homebrew prefix's nginx/servers/)# Minimum memory: 8 GB, which is what the models behind it need; NGINX itself needs almost# nothing# Assumes: the LiteLLM gateway from Part 9 listening on 127.0.0.1:4000, a certificate and# key you already have, and NGINX with the http_ssl_module. NGINX does not read# environment variables from its configuration, so the three values that differ# per machine - the server name and the two certificate paths - are written here# as examples and you edit them once. Nothing secret belongs in this file: the# key is referenced by path, never inlined, and the API keys live in LiteLLM.
# ---------------------------------------------------------------------------------------# The rate-limiting zone. NGINX's documentation gives the syntax as# limit_req_zone key zone=name:size rate=rate;# and notes that one megabyte holds about 16,000 of the 64-byte states that# $binary_remote_addr produces. One request per second sounds absurdly low until you# remember that a chat request occupies a model for seconds, and that the burst below is# what absorbs a person typing quickly. This directive belongs in the http context: if# your distribution's nginx.conf includes conf.d/*.conf inside http, it belongs here; if# it includes it at the top level, move this one line into nginx.conf itself.limit_req_zone $binary_remote_addr zone=llmapi:10m rate=2r/s;
# Redirect the plain-HTTP port rather than serving on it. A client that talks to port 80# has already sent its key in clear text, so the redirect is a convenience for humans# typing an address, not a security control.server { listen 80; server_name gateway.home.arpa; return 301 https://$host$request_uri;}
server { listen 443 ssl; server_name gateway.home.arpa;
# NGINX's HTTPS guide gives this pair as the minimum. The certificate can come from # your own certificate authority for a name that only exists inside the house, or from # a public one for a name that resolves publicly; the directives are the same either # way. Since nginx 1.27.3 the default protocols are TLSv1.2 and TLSv1.3, which is what # you want, so they are not repeated here. ssl_certificate /etc/ssl/local/gateway.crt; ssl_certificate_key /etc/ssl/local/gateway.key;
# The documented optimisation for a machine serving more than one client: one megabyte # of this cache holds about 4000 sessions, so handshakes are not repeated all day. ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;
# A prompt is small. A prompt with a hundred-megabyte "document" attached is somebody # finding out what your machine does when it runs out of memory. Raise this # deliberately if you serve long documents, and know what you raised it to. client_max_body_size 2m;
# Nothing about the gateway's internals belongs in a response header. server_tokens off;
location / { # burst absorbs a short flurry from one client; requests beyond it are refused # rather than queued, because a queued LLM request is a request that will time out # somewhere less visible. The refusal status is 503 by default, which the # documentation states and which is what your dashboard will show. limit_req zone=llmapi burst=10 nodelay;
proxy_pass http://127.0.0.1:4000;
# Version 1.1 is the documented default and is what keepalive connections need. proxy_http_version 1.1;
# Without these the gateway's log records NGINX as every caller, and the rate # limit you configured is the only place a client's address still exists. proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
# Token streaming is a long response sent slowly. Buffering it means the reader # sees nothing and then sees everything, which reads as a hang. proxy_buffering off;
# The documented default read timeout is 60 seconds, which is shorter than a cold # model load. This is the single most common cause of "the gateway works from the # machine itself and times out from anywhere else". proxy_read_timeout 600s; proxy_send_timeout 600s; }
# The two cheap health endpoints, exempt from the rate limit so that a monitor polling # every fifteen seconds does not consume a real client's allowance. Neither of them # touches a model and neither of them needs a key, which is exactly why they are the # ones a monitor should poll. location /health/liveliness { proxy_pass http://127.0.0.1:4000; access_log off; }
location /health/readiness { proxy_pass http://127.0.0.1:4000; access_log off; }
# Prometheus, Grafana, llama-swap's web page and LiteLLM's own /metrics are operator # surfaces. They are not published here, and the stack keeps them on the loopback # address. If you need them from another machine, reach the machine over a VPN rather # than adding a location block to this file.}Four things in it are worth reading twice, because each corresponds to a failure people hit and then misdiagnose.
proxy_buffering off is there because token streaming is a long response delivered slowly,
and the documented default buffers it. A reader who sees nothing for twenty seconds and
then sees everything reports the service as hanging.
proxy_read_timeout 600s overrides a documented default of 60 seconds. A cold model load
plus a long generation exceeds a minute easily, and the symptom is the memorable one: it
works from the machine itself and times out from everywhere else.
client_max_body_size is a security control as much as an operational one. It is the
cheapest possible answer to somebody pasting a very large document into a service that will
try to tokenise all of it.
And the health endpoints get their own location blocks with the rate limit not applied, because a monitor polling every fifteen seconds should not consume a real client’s allowance.
Define what a fallback is allowed to change
Section titled “Define what a fallback is allowed to change”A fallback can preserve availability while changing model quality, context support, output schema reliability or privacy location. Define permitted substitutions per alias. A local-only application must not route to a hosted provider simply because the local worker is unavailable.
Test the route with the primary healthy, loading, unavailable and over capacity. Record whether the gateway queues, rejects, retries or selects another model. A retry needs a bound, and a request that can trigger external effects needs an idempotency policy above the model service.
Separate liveness, readiness and task correctness. A process can be alive before its model loads; a ready engine can still fail your required tool-call contract. Keep a lightweight health check and a deeper application probe. Record alias-to-artefact mappings so a fallback result can be attributed to the model that actually answered. A stable client name is useful only when the operator can still reconstruct its routing decisions.
Four layers, each answering one question: NGINX for transport and abuse, LiteLLM for
identity and policy, llama-swap for what is loaded, engines for the model itself. llama-swap
turns memory into a schedule through ttl, unloadTimeout and groups, and its
checkEndpoint and healthCheckTimeout are what let it distinguish a started engine from a
ready one. Rolling a new model in without downtime means versioning the model id, warming
the new one, comparing it on your own evaluation set, and then moving the alias by one line.
LiteLLM’s virtual keys, budgets and per-minute limits need a database and are worth the
database; its routing strategies are how one alias covers two machines. Health means three
different things and asking the expensive one on a timer is how a monitor brings down a
swapping gateway. Fallbacks trade a visible failure for a quiet degradation, which is right
for a person and wrong for a pipeline. And the front door terminates TLS, refuses oversized
bodies, limits request rate per client, and does not buffer the stream.
Check your understanding
Sources for this lesson
12 verified · checked 2026-09-09
- 01llama-swap — README§ Endpoints; configuration keys; groupsgithub.com/mostlygeek/llama-swap2026-09-09
- 02llama-swap — example configuration§ healthCheckTimeout; checkEndpoint; ttl; unloadTimeout; groupsgithub.com/mostlygeek/llama-swap/blob/main/docs/config.example.yaml2026-09-09
- 03LiteLLM — Proxy config.yaml§ model_list; os.environ references; routing_strategy; model_group_aliasdocs.litellm.ai/docs/proxy/configs2026-09-09
- 04LiteLLM — Reliability and fallbacks§ fallbacks; context_window_fallbacks; num_retries; allowed_fails; cooldown_timedocs.litellm.ai/docs/proxy/reliability2026-09-09
- 05LiteLLM — Health checks§ Endpoints; background health checksdocs.litellm.ai/docs/proxy/health2026-09-09
- 06LiteLLM — Virtual keys§ Requirements; key generationdocs.litellm.ai/docs/proxy/virtual_keys2026-09-09
- 07LiteLLM — Budgets and rate limits§ max_budget; budget_duration; tpm_limit; rpm_limitdocs.litellm.ai/docs/proxy/users2026-09-09
- 08llama.cpp — llama-server README§ --api-key; --props; --slots; /healthgithub.com/ggml-org/llama.cpp/blob/master/tools/server/README.md2026-09-09
- 09nginx — ngx_http_limit_req_module§ limit_req_zone; limit_req; limit_req_statusnginx.org/en/docs/http/ngx_http_limit_req_module.html2026-09-09
- 10nginx — ngx_http_proxy_module§ proxy_pass; proxy_set_header; proxy_buffering; proxy_read_timeoutnginx.org/en/docs/http/ngx_http_proxy_module.html2026-09-09
- 11nginx — Configuring HTTPS servers§ The minimal server block; session cachenginx.org/en/docs/http/configuring_https_servers.html2026-09-09
- 12Caddy — Automatic HTTPS§ The internal certificate authority; caddy trustcaddyserver.com/docs/automatic-https2026-09-09
Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.