A protocol-agnostic load balancing strategy engine for Rust.
Pick the right node — nothing more, nothing less.
loadwise answers one question: "given these nodes, which one should handle the next request?"
It ships 7 strategies, a health state machine, composable filters, and a pluggable state store — all in a synchronous, zero-allocation hot path. No async runtime, no tracing crate, no metrics dependency.
use loadwise::{Strategy, SelectionContext};
use loadwise::strategy::RoundRobin;
let strategy = RoundRobin::new();
let backends = ["10.0.0.1:8080", "10.0.0.2:8080", "10.0.0.3:8080"];
let ctx = SelectionContext::default();
let idx = strategy.select(&backends, &ctx).unwrap();
println!("route to {}", backends[idx]);Most load balancers live inside a proxy. loadwise gives you the algorithm without the infrastructure — embed it in any Rust application, from an API gateway to an LLM router to a game server matchmaker.
- Zero-cost abstraction —
Strategy::selectreturns anOption<usize>, not a reference. No allocation on the hot path. - Protocol-agnostic — works with HTTP, gRPC, WebSocket, database connections, or anything you can put in a slice.
- Three opt-in traits —
Node(identity),Weighted(weight),LoadMetric(load score). Implement only what your strategy needs. - No opinion on I/O — no async runtime, no HTTP client, no health-check poller. You report events; loadwise manages state.
| Strategy | Requires | Algorithm |
|---|---|---|
RoundRobin |
— | Atomic sequential cycling |
WeightedRoundRobin |
Weighted + Node |
Nginx smooth weighted round-robin |
Random |
— | Uniform random |
WeightedRandom |
Weighted |
Proportional random by weight |
LeastLoad |
LoadMetric |
Pick the node with lowest load_score() |
PowerOfTwoChoices |
LoadMetric |
Randomly sample 2, pick the less loaded — O(1) |
ConsistentHash |
Node |
Virtual-node hash ring with caching |
Compose strategies with WithFallback (generic, zero-cost) or FallbackChain (dynamic dispatch):
use loadwise::strategy::{LeastLoad, RoundRobin, WithFallback};
// Try least-load first; if no candidates remain after filtering, fall back to round-robin.
let strategy = WithFallback::new(LeastLoad, RoundRobin::new());A four-state machine with configurable thresholds:
┌─────────┐ failures ≥ ⌈t/2⌉ ┌──────────┐ failures ≥ t ┌───────────┐
│ Healthy │ ─────────────────→ │ Degraded │ ────────────────→ │ Unhealthy │
└─────────┘ └──────────┘ └───────────┘
↑ │ │
│ recovery_successes met │ 1 success │ 1 success
│ ↓ ↓
│ ┌─────────┐ ┌────────────┐
└──────────────────────────│ Healthy │←── recoveries ────│ Recovering │
└─────────┘ └────────────┘
use loadwise::{HealthTracker, HealthStatus, ConsecutiveFailurePolicy};
let tracker = HealthTracker::new(ConsecutiveFailurePolicy::default());
tracker.report_failure(&"node-1");
tracker.report_failure(&"node-1");
tracker.report_failure(&"node-1");
assert_eq!(tracker.status(&"node-1"), HealthStatus::Unhealthy);
tracker.report_success(&"node-1");
assert_eq!(tracker.status(&"node-1"), HealthStatus::Recovering);Narrow candidates before selection — zero-allocation bitmask filters:
use loadwise::filter::{HealthFilter, AllOf, FnFilter, Filter};
// Only route to healthy + recovering nodes
let filter = HealthFilter::available();
// Combine filters with AND logic
let combined = AllOf::new(vec![
Box::new(HealthFilter::healthy_only()),
Box::new(FnFilter(|node: &MyNode| node.region == "us-east")),
]);Bring your own metrics stack — loadwise provides a hook, not a dependency:
use loadwise::metrics::MetricsCollector;
struct PrometheusCollector;
impl MetricsCollector for PrometheusCollector {
fn on_selection(&self, node_id: &dyn std::fmt::Debug, strategy: &str) {
// prometheus::counter!("lb_selections", "strategy" => strategy).inc();
}
fn on_health_change(&self, node_id: &dyn std::fmt::Debug,
from: loadwise::HealthStatus, to: loadwise::HealthStatus) {
// record transition
}
}loadwise/
├── loadwise-core Core traits, 7 strategies, health, filters, state store
├── loadwise Facade — re-exports core with optional feature flags
├── loadwise-store-redis Redis-backed StateStore (HASH-based, JSON-serialised)
└── loadwise-tower Tower Layer / Service adapter (planned)
| Crate | Description |
|---|---|
loadwise |
Facade — re-exports core, optional tower / redis features |
loadwise-core |
All logic: traits, strategies, health, filters, state store |
loadwise-store-redis |
Redis-backed StateStore via single HASH |
loadwise-tower |
Tower Layer / Service adapter (planned) |
[dependencies]
loadwise = "0.1"MSRV: 1.85 (Rust edition 2024)
select()is synchronous — returns an index into the candidate slice. No futures, no allocation.- Interior mutability — strategies use
AtomicUsizeorMutexinternally; callers hold&self. - Fingerprint-based caching — stateful strategies (hash ring, WRR weights) detect candidate-set changes automatically via a hash of node IDs.
- Pluggable everything —
StateStorefor persistence,HealthPolicyfor state transitions,MetricsCollectorfor observability. Implement the trait, bring your own backend.
Contributions are welcome! Please make sure cargo test passes and new public items have /// doc comments with examples.
Licensed under either of Apache License, Version 2.0 or MIT License at your option.