--sync-store=valkey the routers of one site share their usage through a Valkey store and enforce each limit together.
What is shared
Every router keeps deciding in memory, exactly as a single router does. The only time a request waits on the store is the first request of the day for a key with a daily limit, which reads that key’s total once: at most 200 ms, and requests for the same key share the read.
How it works
Every--sync-interval (default 1s) each router:
- publishes the usage it admitted since the last round as one entry on a stream in Valkey (nothing when it admitted nothing);
- reads the entries the other routers published and subtracts them from its own buckets;
- writes its own running total of today’s tokens for each daily bucket it used (a bucket’s total is the sum over routers, so a write lost in a failover is healed by the next one);
- records a heartbeat, which tells every router how many routers are live.
Occupancy credits
The occupancy budgets must never be exceeded, even for a moment, so they are not synced like the rate limits. For each model the store holds a pool: how much of the budget each router holds right now. Each router keeps a wallet of credits in memory and admits against it, so a request never waits on the store.- When its wallet runs short, a router asks the pool for more. The pool grants only from the part of the budget no router holds, so the routers together never hold more than the budget. With Sentinel, a replica acknowledges each grant, so a failover cannot hand the same capacity out twice. When no replica acknowledges in time, the grant is used but the router stays at its share of the budget until a replica acknowledges again, so admission never stops because replicas lag.
- A wallet no request has used for a minute hands its credits back and is dropped.
- Each round a router keeps what it uses plus a small headroom and hands back the rest. A router that found its wallet empty flags it, and the others then keep no headroom, so the budget follows the traffic.
- Streamed output that grows a request past its credits is recorded as held on the next round.
A router that was given up by the others drops the credits it believed it held and asks the pool again. A router cut off from the store admits nothing more once an agent connects to it again: that agent may have reported it gone in the meantime, and the others may have taken its credits back. It admits again once the store answers.
A router that fails together with one of its agents, for example when a whole node goes down, keeps its credits until it restarts under the same
--router-id: the site runs with that much less budget meanwhile. It is never over budget.When the store is unreachable
Output tokens are counted after the response, so they are never refused because of a router’s share. They are refused only when the shared daily budget is spent, as with one router.
Deploy Valkey
Use one Valkey per site, reachable only from the routers. For high availability run one primary, two replicas and three Sentinels on separate hosts. Sentinel promotes a replica when the primary fails; routers ask the Sentinels for the current primary. The store holds only recent usage, today’s totals and the occupancy credits. After a total loss the routers rebuild from what they hold in memory, but for the settle window (about 75 s with the default 60 s request timeout) a model or key no router had served before the loss gets429 on every router. To keep restarts from causing that, run the primary with persistence (AOF, appendonly yes) and replicas behind Sentinel, so a restart or a failover keeps the data; a store that keeps its data is not treated as new. They stay within the budget as long as a router cut off from the store when it lost its data comes back, or ends its requests, within the settle window. One that stays cut off longer, and whose agents no other router hears from, can add up to its own share of the budget. Grants are acknowledged by a replica; the per-round rewrites of what each router holds are not, and are healed by the next round.
Configure the routers
Give every router of the site the same--sync-site (it prefixes every key in the store, as a hash tag: hivenet:{site-a}:…; up to 64 letters, digits, ., _ or -; it defaults to --site-id, else default) and a stable --router-id. Set --sync-min-routers to the number of routers: the router cannot know it while the store is unreachable, and with the default 1 routers that start during an outage each enforce the whole limit (the router logs a warning). In Kubernetes, set it to the StatefulSet’s replica count; a StatefulSet pod name works as --router-id; the default is the hostname.
--sync-site names the site in the store only. --site-id does too when --sync-site is not set, but it also turns on cross-site redirect, which needs redirect secrets, allowed domains, and dynamic API keys or auth mode none.
--sync-store=valkey replaces HIVENET_ROUTER_QUOTA_BACKEND=badger: the store keeps today’s totals. The router refuses to start with both.
Connect agents to every router
Give each agent the gRPC address of every router of the site with--routers (or HIVENET_AGENT_ROUTERS):
- A router that is down or restarting does not affect the agent’s sessions with the other routers.
- A router that comes up later is picked up automatically.
- The agent polls the engine metrics and checks the backend health once, for all sessions.
- Without discovery, each address reaches one specific router, never the client load balancer or a Kubernetes ClusterIP Service. Through a load balancer the agent gets a single session to whichever router answers, and the other routers never see the agent; nothing reports it. In Kubernetes, use the pod DNS names of a headless Service (
router-0.router-headless:50051, …). With discovery, the list is only where the agent starts: it finds every router from the first one it reaches. - List every router, and update the list when you add one, or let the agents find every router. Every router divides the occupancy budgets by the live routers, whether or not agents reach it: a router no agent connects to lowers the others’ share and cannot serve requests itself.
- Every router of the site uses the same JWT secret.
- Each router sends its libp2p address in the authentication response. A router behind NAT must set
--p2p-announce-addrto an address the agents can reach.
--router-grpc still connects an agent to a single router; it is used when --routers is empty.
Let agents find every router
Keeping every agent’s--routers up to date is easy to get wrong. Instead, set --advertise-grpc-addr on every router to the address agents reach it at:
:50051 or 0.0.0.0:50051 is refused at startup.
Each router publishes its address with its heartbeat in the store. Every router hands the agents the live routers of the site that advertise one: in the authentication response, then in every heartbeat reply. An agent then:
- opens a session with every listed router it has no session with, so a router added to the site gets the agents within one heartbeat (default 5 s);
- needs only one reachable router in
--routers, which becomes the starting list (it may be a DNS name); - recognizes a router it already reaches under another address (a
--routersDNS name, the router listed under its pod IP) by its peer ID, and opens no second session to it. A router listed under a new address is dialed there once its old session is down; - closes the session of a discovered router only once that router has been missing from every list for 2 minutes and its session is down, or at once when one of its
--routersentries already reaches it. A short or empty list (a router restarting) changes nothing, and the routers in--routersare never closed.
--advertise-grpc-addr are not listed, and agents keep the sessions from their --routers.
Each agent reports how many requests it has in flight from all routers. Every router ranks agents by the larger of that report and its own in-flight requests, so the routers spread work by the agent’s real load.
Monitor
Suggested alerts:
hivenet_sync_peers to the number of routers you run minus one.
