Load-Balance a Multi-Node Ollama Cluster With Olla

Put Olla in front of a multi-node Ollama cluster for one endpoint with load balancing, automatic failover, a unified model list, and Prometheus metrics.

On this page
  1. What Olla is (and isn’t)
  2. Deploy Olla
  3. One catalog, and a receipt for every request
  4. Failover you can actually watch
  5. Wire it into Prometheus (no extra exporter)
  6. The flourish: point Claude Code at your own cluster
  7. What’s next

I ended up with three Ollama boxes almost by accident — one with a bit of GPU, two smaller ones for embeddings — and then spent weeks quietly annoyed at my own setup. Every tool I pointed at “my local AI” had to be told a specific node’s address. If that node was busy or down, the tool just failed; nothing rerouted. And no two nodes had the same models, so I kept a mental map of which box held which model. Three servers, zero coordination.

Olla fixes exactly that. It’s a small, fast proxy and load balancer that sits in front of your inference backends and gives you one endpoint — with health checks, automatic failover, a merged model catalog, and Prometheus metrics baked in. This is the first of a set of “next-level” homelab builds, and it’s the one I wish I’d deployed the day I added my second Ollama node. Everything below I ran on my own three-node cluster; the routing receipts and metrics you’ll see are real output.

The Olla dashboard 'herd status' overview: status healthy, 3 of 3 endpoints up, olla engine with least-connections balancing, 7 models discovered across 3 ollama backends, and a herd-at-a-glance table listing ollama-node1, ollama-node2, and ollama-node3 all healthy.
Olla's built-in dashboard (/internal/ui/): one endpoint in front of three Ollama nodes, all healthy, with a live model count and per-node status.
First: make these values your own

Every address below is an example. Replace 10.0.0.61/62/63 with your own Ollama nodes’ IPs, and 10.0.0.20 with the host you run Olla on. Model names like gemma3:4b should be whatever you’ve actually pulled. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.


What Olla is (and isn’t)

Olla is a load balancer and reverse proxy for LLM backends. It doesn’t serve models itself — your Ollama nodes still do that. Olla sits in front and makes several independent servers behave like one: it continuously health-checks each backend, merges their model lists into a single catalog, routes each request to a healthy node that actually has the requested model, and retries on another node if a connection fails. It speaks Ollama-native and OpenAI-compatible APIs, and — a genuinely nice touch — translates the Anthropic Messages API by default, so Claude-flavored tools can talk to your local models too.

If you run exactly one Ollama box, you don’t need this. The moment you run two, you do — because “which node do I point at, and what happens when it’s down?” is a question you shouldn’t have to answer by hand.


Deploy Olla

Olla ships as a single container image, so a small VM or LXC with Docker is all you need.

1Run the container3 min

Pull and run the official image, publishing its port (40114 — “4-OLLA”, which made me smile). I use --network host so the built-in dashboard, which is loopback-aware, is reachable.

Start Olla

docker run -d --name olla --network host \
-v ~/olla/config.yaml:/app/config/config.yaml \
ghcr.io/thushan/olla:latest
The config path that cost me ten minutes

Olla reads its config from config/config.yaml relative to its working directory — inside the container that’s /app/config/config.yaml. I first mounted mine at /etc/olla/config.yaml (a reasonable guess) and Olla silently loaded its built-in default instead, happily health-checking three backends I’d never configured. If your nodes don’t show up, check the startup log line Loaded configuration config=config/config.yaml and mount to that path.

2Point it at your Ollama nodes5 min

The config is small. List each backend under discovery.static.endpoints, and pick a load-balancing strategy. I give my GPU node the highest priority so it’s preferred, with the two smaller nodes as fallbacks.

~/olla/config.yaml

server:
host: "0.0.0.0"
port: 40114
proxy:
engine: "olla"
load_balancer: "least-connections"   # or: priority, round-robin
discovery:
type: "static"
static:
  endpoints:
    - url: "http://10.0.0.61:11434"
      name: "ollama-node1"
      type: "ollama"
      priority: 100
    - url: "http://10.0.0.62:11434"
      name: "ollama-node2"
      type: "ollama"
      priority: 90
    - url: "http://10.0.0.63:11434"
      name: "ollama-node3"
      type: "ollama"
      priority: 80

The three strategies map to three intents: priority (“always prefer the big node”), round-robin (“spread it evenly”), and least-connections (“send it wherever there’s the least in-flight work”). I run least-connections because my nodes are uneven and I’d rather chase idle capacity than a fixed order.

3Confirm all your nodes are healthy1 min

Olla’s /internal/status/endpoints shows each backend’s health and how many models it found. This is the real output from my three-node cluster:

Every node up, models discovered

$ curl -s localhost:40114/internal/status/endpoints
ollama-node1  healthy  models=5  http://10.0.0.61:11434
ollama-node2  healthy  models=1  http://10.0.0.62:11434
ollama-node3  healthy  models=1  http://10.0.0.63:11434

One catalog, and a receipt for every request

Two things make Olla feel like magic the first time. First, the unified model catalog: ask Olla for its models and you get every model across every node in one list, deduplicated — no more remembering which box holds which model.

The Olla dashboard Models tab: a unified catalog listing llama3.1:8b, qwen2.5:14b, mistral-small:22b, llava:13b, nomic-embed-text, llama3.2:3b and phi3:mini, each with its parameter size, quantization, file size, and which node (ollama-node1, ollama-node2, or ollama-node3) serves it.
The unified model catalog — every model across all three nodes in one deduplicated list, each tagged with the endpoint that serves it.

The second is my favorite: every proxied response comes back with X-Olla-* headers that tell you exactly what happened. Here’s a real request I sent through Olla, asking gemma3:4b a trivial question, with the routing headers it returned:

A real routed request (headers are Olla's, unedited)

$ curl -sD - localhost:40114/olla/openai/v1/chat/completions \
  -d '{"model":"gemma3:4b","messages":[{"role":"user","content":"Reply OK"}],"stream":false}'

HTTP/1.1 200 OK
X-Olla-Endpoint: ollama-node1
X-Olla-Model: gemma3:4b
X-Olla-Backend-Type: ollama
X-Olla-Routing-Decision: routed
X-Olla-Routing-Reason: model_found
X-Olla-Response-Time: 10674ms
...
{"choices":[{"message":{"content":"OK"}}]}

That X-Olla-Endpoint: ollama-node1 is Olla telling you, per request, which backend actually served it. When you’re debugging “why was that answer slow,” a receipt naming the node and the response time is worth a great deal.

Proxy routes live under /olla/<provider>/

The one thing that isn’t obvious: Olla proxies under a provider-prefixed path, not the bare API path. Use /olla/openai/v1/chat/completions for the OpenAI-compatible route and /olla/ollama/api/chat for the Ollama-native one. A plain POST /v1/chat/completions returns 404 — I probed my way to this, so you don’t have to.


Failover you can actually watch

Health checks are only interesting if something acts on them. Olla’s retry logic (retry.on_connection_failure, on by default) means that if the node it picked refuses the connection, it transparently tries another healthy node instead of failing the request. Pull the plug on a node and requests keep flowing; the model list shrinks to what’s left standing.

A node drops — the request doesn’tOllaretry on failureollama-node2offline — health check failedollama-node1healthy — retried here, 200 OK1. refused2. retriedThe client sees one successful response and never learns a node was down.

Wire it into Prometheus (no extra exporter)

This is where Olla earns its place in a monitored homelab: it exposes native Prometheus metrics at /internal/metrics, so your existing Prometheus scrapes it directly and you graph it in Grafana. No sidecar, no exporter. After my test requests, the counters looked like this:

Real /internal/metrics output

$ curl -s localhost:40114/internal/metrics | grep '^olla_'
olla_endpoints_total 3
olla_endpoints_healthy 3
olla_requests_total 12
olla_model_requests_total{model="gemma3:4b"} 1

A one-line scrape job turns those into graphs and alerts — a panel for healthy-endpoint count (alert if it drops below your node total), request rate, and per-model usage so you can see which model your household actually leans on.

prometheus.yml — scrape Olla

scrape_configs:
- job_name: olla
  static_configs:
    - targets: ["10.0.0.20:40114"]
  metrics_path: /internal/metrics
Lock the dashboard down to your LAN

Olla’s read-only dashboard at /internal/ui/ is gated by an allow-list: a request must come from an allowed CIDR and present an allowed Host header. The defaults already permit the private ranges and localhost, so it loads over your LAN without fuss — but if the host sits on an untrusted network, tighten dashboard.access_policy.allowed_cidrs to just the addresses you trust.


The flourish: point Claude Code at your own cluster

Here’s the bonus that made me grin. Because Olla translates the Anthropic Messages API by default and gives you one stable endpoint, you can aim Claude-flavored coding tools at your local models — with failover across nodes — instead of a metered cloud key. For long multi-turn sessions there’s even opt-in sticky-session routing (sticky_sessions) that keeps a conversation pinned to the same backend so its KV-cache stays warm. One endpoint, three nodes, and your coding assistant never notices which one answered.


What’s next

You now have one address for “my local AI,” with failover and graphs behind it. Next in this run of flagship builds, I give my homelab itself an API an AI can safely read: a read-only MCP server that lets Claude query my cluster’s health, metrics, and boards — without ever being able to change a thing.


Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.