-
Notifications
You must be signed in to change notification settings - Fork 1
Routing Algorithms
Deep dive into how VelocityNavigator decides which server to send a player to.
| Algorithm | Distribution Quality | CPU Cost | Requires Health Data | Sticky Sessions | Best For |
|---|---|---|---|---|---|
least_players |
★★★★★ | Medium | Yes | No | Small-medium networks |
power_of_two |
★★★★☆ | Low | Yes | No | Medium networks, default pick |
round_robin |
★★★☆☆ | Very Low | No | No | Testing, strict fairness |
random |
★★★☆☆ | Very Low | No | No | Large-scale networks |
weighted_round_robin |
★★★★☆ | Low | No | No | Unequal server capacity |
least_connections |
★★★★★ | Medium | Yes | No | Bursty traffic, large networks |
consistent_hash |
★★★☆☆ | Low | No | Yes | Session affinity, party routing |
latency |
★★★★★ | Medium | Yes | No | Lowest ping, regional networks |
Picks the server with the fewest connected players.
Complexity: O(n) — scans all candidates each selection.
When to use: Most networks. This is the default selection mode because it produces the most even distribution when you have a small-to-medium number of servers.
When NOT to use: Very large server pools (50+) where scanning every server adds measurable latency, or when you need deterministic player-to-server mapping.
Note: While
least_playersis the code default,power_of_twois the recommended default for most production networks (see below).
Example (10 players → 3 servers):
lobby-1: ████ (4 players)
lobby-2: ███ (3 players)
lobby-3: ███ (3 players)
Picks two random candidates, then selects the one with fewer players.
Complexity: O(1) — only examines two servers.
When to use: Medium-sized networks (4–10 servers). Provides near-optimal distribution at a fraction of the cost of least_players. This is the recommended default for most production networks.
When NOT to use: Very small networks (2 servers — it degenerates to least_players) or when you need perfectly even distribution.
Example (10 players → 3 servers):
lobby-1: ████ (4 players)
lobby-2: ███ (3 players)
lobby-3: ███ (3 players)
Nearly identical to least_players at low load, but scales much better.
Cycles through servers in strict order using an atomic counter.
Complexity: O(1) — no scanning, just increment and modulo.
When to use: Testing, benchmarking, or when you need perfectly deterministic rotation. Good when all servers have identical capacity.
When NOT to use: Production networks where servers have different capacities, or when players join in bursts (causes temporary imbalance).
Example (10 players → 3 servers):
lobby-1: ████ (4 players) ← players 1, 4, 7, 10
lobby-2: ███ (3 players) ← players 2, 5, 8
lobby-3: ███ (3 players) ← players 3, 6, 9
Each player is assigned a completely random server.
Complexity: O(1) — single random selection.
When to use: Very large networks (50+ servers). At scale, the law of large numbers guarantees roughly even distribution. Zero coordination overhead between proxy instances.
When NOT to use: Small networks where random variance produces noticeable imbalance, or when you need any kind of deterministic behavior.
Example (10 players → 3 servers):
lobby-1: █████ (5 players) ← random variance
lobby-2: ███ (3 players)
lobby-3: ██ (2 players)
Variance evens out as player count grows.
Like round-robin, but servers with higher weight receive proportionally more players. Uses interleaved WRR to avoid burst clustering.
Complexity: O(n) per round cycle, O(1) amortized per selection.
When to use: When your servers have different capacities. Set weight higher on beefier servers so they receive more traffic.
When NOT to use: When all servers are identical (just use regular round_robin or power_of_two).
Example (10 players → 3 servers, weights: lobby-1=3, lobby-2=2, lobby-3=1):
lobby-1: █████ (5 players) ← weight 3
lobby-2: ███ (3 players) ← weight 2
lobby-3: ██ (2 players) ← weight 1
Configure weights using inline table format:
default_lobbies = [
{ server = "lobby-1", weight = 3 },
{ server = "lobby-2", weight = 2 },
{ server = "lobby-3", weight = 1 },
]Selects the server with the lowest exponential moving average (EMA) of active connections and connection rate over time.
Complexity: O(n) — scans all candidates with EMA computation.
When to use: Networks with bursty traffic patterns. EMA smooths out momentary spikes, making this more stable than least_players during traffic surges.
When NOT to use: Very small or very stable networks where least_players or power_of_two are simpler and equally effective.
Example (10 players → 3 servers, with burst traffic):
lobby-1: ███ (3 players) ← EMA low, receives next player
lobby-2: ████ (4 players) ← EMA elevated from recent burst
lobby-3: ███ (3 players) ← EMA low
Hashes the player's UUID onto a consistent hash ring (150 virtual nodes, SHA-256). The same player always lands on the same server unless that server is removed.
Complexity: O(log n) — ring lookup.
When to use: When you need sticky sessions — players returning to "their" server. Great for party routing, inventory caching, or any system where player-server affinity matters.
When NOT to use: When you need perfectly even distribution (hash distribution has natural variance), or when you don't need sticky sessions.
Example (10 players → 3 servers):
lobby-1: ████ (4 players) ← hash ring assignment
lobby-2: ███ (3 players)
lobby-3: ███ (3 players)
Same player always goes to the same server. Adding/removing servers only remaps a fraction of players.
Picks the server with the lowest ping latency measured during health check pings.
Complexity: O(n) — scans all candidates each selection to find the minimum ping.
When to use: Multi-regional proxy networks or when players should always connect to the geographically closest/lowest latency lobby server.
When NOT to use: When you want even player distribution, as players in the same geographical region will naturally group onto the same low-ping server.
Example (10 players → 3 servers, pings: lobby-east=25ms, lobby-west=70ms, lobby-eu=110ms):
Players from the East Coast are routed to lobby-east (25ms), West Coast to lobby-west (70ms), and EU to lobby-eu (110ms) to ensure optimal gameplay quality.
All algorithms that require real-time data (least_players, power_of_two, least_connections) rely on player count information. VelocityNavigator uses live player counts from RegisteredServer.getPlayersConnected() for routing decisions, ensuring accurate and up-to-date load information.
The health check cache serves as an online/offline filter — servers marked as offline by health checks are excluded from the candidate pool. Health checks run on a configurable interval (default: 60 seconds) with a cache warming task that runs at 80% of the TTL to ensure data freshness.
When the circuit breaker opens for a server (after repeated failures), that server is also excluded — even if its health check cache hasn't expired yet.
When all candidate servers fail health checks and no selection can be made, VelocityNavigator can fall back to a degradation mode (default: random) that ignores health status and selects from all configured lobbies. This prevents the "No lobby found" error during total outages.
[degradation]
enabled = true
mode = "random"See Configuration Guide for details.
Contextual routing groups can override the global selection mode. For example, your main lobbies use power_of_two but your BedWars lobbies use consistent_hash so players return to the same lobby:
[routing.contextual.groups.bedwars_lobbies]
servers = ["bw-1", "bw-2"]
mode = "consistent_hash"See Contextual Routing Guide for the full tutorial.
🏠 Home · ⚙️ Configuration · 🔌 Developer API · 📋 Operations Runbook · ❓ FAQ
GitHub · Support / Discord · 🐛 Report a Bug
VelocityNavigator v4.2.0 · by DemonZ Development
![]()
Getting Started
Routing
Configuration
Features
Operations
Developer
Maintained by DemonZ Development