# How the viiwork mesh routes a request
URL: https://viiwork.pulu.app/blog/how-the-mesh-routes-requests/
Updated: 2026-09-15

A viiwork node serves the models configured on its own machine and keeps track of what every other node serves. That is what lets a client use one [OpenAI-compatible endpoint](/docs/api-integration/) for the whole fleet, without a list of machines or backends of its own. This post follows a request through the mesh, using the rules in the [mesh documentation](/docs/mesh/) for viiwork v2.11.0.

## Nodes find each other

There are no peer lists. On a Tailscale tailnet, which is the default, a node asks the local tailscaled for the other online machines and contacts them. On a plain LAN it [finds them with mDNS](/docs/autodiscovery/). Where neither reaches, a few seed addresses in the config are enough. Discovery repeats every minute, which is also how two halves of a split network merge again.

Every machine uses two fixed ports: 8086/tcp for the API and the dashboards, and 7946 over both tcp and udp for membership gossip. In a [secured mesh](/docs/security/) every machine has the same `VIIWORK_MESH_SECRET`. Gossip is then encrypted and authenticated, a node without the secret cannot join, and requests forwarded between nodes are signed. An open mesh has no secret and has to be declared with `mesh.open: true`; a node that has neither refuses to start.

## The entry node looks at capacity

Every node is an entry point. Membership travels by gossip, but capacity is polled: each node asks every live member for its [`/v1/capacity`](/docs/consuming-fleet-capacity/) once a second. When a request arrives, the node that received it looks for hosts that serve the requested model and have a free slot. A report older than three seconds is not used for routing, so a machine that has just gone quiet stops getting work before it leaves the membership list.

With performance routing, which is on by default, each node measures its own time to first token for each model and publishes the result. For a request, every host with a free slot gets a predicted time from its score and the size of the prompt, and only hosts within 1.25 times the best prediction are considered. If the entry node is one of them, it runs the request itself. Otherwise faster hosts are picked more often. A host with no score yet is priced at the fleet's median and gets one request in twenty, so it can earn a score of its own. When no host has a score, the request goes to a free local backend, else to the member with the most free slots.

## A forward happens once

When the request goes to another machine, the receiving node admits it only into a free local slot. If it has none, it answers 429 at once. It never queues a forwarded request and never forwards it again, so a request cannot bounce around the mesh. A forward that is refused before its first byte (a 429, a 503 or no connection) is retried on another route, and that member is treated as full for the model until a newer capacity report arrives.

## When every slot is busy

The entry node puts the request in a first-in, first-out queue for that model and waits up to 20 seconds (`routing.queue_timeout`) for a slot anywhere in the mesh. If none opens, the client gets HTTP 429 with `Retry-After: 2`.

## Steering a request

Most clients can let the mesh choose. A coding agent should send `X-Session-Affinity` or `X-Session-Id` with a value that stays the same for the session: its turns then go to the same host, whichever node they enter by, and [reuse that host's prompt cache](/blog/session-affinity-coding-agents/index/). `?host=<node name>` pins a request to one machine, and `?prefer=a,b` names machines to try first when they have room; if none of them does, the request routes as if the list were not there. Both names are only compared with member names and never dialled as addresses.

The [mesh page](/docs/mesh/) has the remaining details, including aliases, the mesh-wide model names that clients can use instead of real model names.
