Vane is a single binary layer 7 reverse proxy and load balancer written in Go. It terminates HTTP and HTTPS, routes requests by host, path and method, spreads traffic across upstream targets with six balancing algorithms, and keeps failing targets out of rotation through active health checks and per target circuit breakers. Configuration is a single YAML file that is reloaded without dropping connections, and the runtime is observable through a built in control plane, a Prometheus endpoint and a command line client.
- Capabilities
- Screenshots
- Architecture
- Quick start
- Configuration
- Load balancing algorithms
- Resilience
- TLS
- Admin API
- Command line client
- Observability
- Deployment
- Performance
- Development
- Roadmap
- License
| Area | What Vane does |
|---|---|
| Host matching | Exact names and single label wildcards such as *.example.com |
| Path matching | Longest prefix wins, on segment boundaries, so /api never matches /apifoo |
| Route controls | Optional method filters, prefix stripping, per route timeouts, request and response header rules |
| Rate limiting | Token bucket per route, keyed by client address, answered with 429 and Retry-After |
| Balancing | Round robin, smooth weighted round robin, weighted least connections, IP hash, consistent hash, weighted random |
| Consistent hashing | 160 virtual nodes per unit of weight, so removing one of four targets remaps a minority of keys |
| Health checking | Per target probes with separate healthy and unhealthy thresholds, configurable path, interval and expected statuses |
| Circuit breaking | Per target breaker with closed, open and half open states |
| Retries | Attempts spread across distinct targets, with a status allow list, backoff and a bounded body buffer |
| Draining | Targets removed from rotation while in flight requests finish, preserved across reloads |
| Configuration | One YAML file, validated before it is applied, reloaded on file change, on SIGHUP and through the API |
| Protocols | HTTP/1.1 and HTTP/2 upstreams, WebSocket upgrades, responses flushed as they stream |
| Forwarded headers | X-Forwarded-For, -Proto, -Host and X-Real-Ip behind a trusted proxy allow list, hop by hop headers stripped |
| TLS | Static certificates reloaded without a restart, or automatic certificates over ACME |
| Observability | Prometheus metrics, structured access logs, request identifiers, a dashboard and a CLI |
The control plane serves a dashboard that reports traffic, latency percentiles, upstream health and route
configuration. The screenshot below was taken while the demo profile in configs/vane.demo.yaml served
roughly 850 requests per second across three upstreams, with one target failing its health check and another
manually drained.
vanectl exposes the same information for terminals and scripts.
Every request is traceable through response headers, Prometheus counters and the structured access log.
Vane separates the request path from the control path. The request path holds no locks beyond a single atomic pointer read, while health checks, configuration reloads and the admin API operate on the control path.
flowchart LR
client([Clients])
subgraph vane[Vane process]
direction TB
listener[HTTP and TLS listeners]
router[Router<br/>host, path, method]
middleware[Rate limit<br/>header rules<br/>timeouts]
balancer[Balancer<br/>six algorithms]
forward[Forwarder<br/>retries and upgrades]
subgraph control[Control path]
direction TB
health[Health checker]
watcher[Config watcher]
admin[Admin API and dashboard]
metricsstore[Metrics registry]
end
end
subgraph upstreams[Upstream pools]
direction TB
api[api targets]
web[web targets]
events[events targets]
end
client --> listener --> router --> middleware --> balancer --> forward
forward --> api
forward --> web
forward --> events
health -.probes.-> api
health -.probes.-> web
health -.probes.-> events
health -.marks healthy or unhealthy.-> balancer
watcher -.atomic swap.-> router
forward -.observations.-> metricsstore
admin -.reads.-> metricsstore
sequenceDiagram
participant C as Client
participant V as Vane
participant B as Balancer
participant T1 as Target A
participant T2 as Target B
C->>V: GET /v1/users
V->>V: match route, assign request id
V->>V: rate limit check
V->>B: pick an available target
B-->>V: Target A
V->>T1: forward with X-Forwarded-* headers
T1-->>V: 502 Bad Gateway
V->>V: record failure, advance circuit breaker
V->>B: pick again, excluding Target A
B-->>V: Target B
V->>T2: retry after backoff
T2-->>V: 200 OK
V-->>C: 200 OK with X-Vane-Upstream and X-Vane-Target
| Path | Responsibility |
|---|---|
cmd/vane |
Server entry point, flags, signal handling |
cmd/vanectl |
Admin API client with table and JSON output |
internal/config |
YAML schema, defaults, validation, file watcher |
internal/router |
Host, path and method matching with specificity ordering |
internal/balancer |
Balancing strategies over an abstract node interface |
internal/upstream |
Target state, pools, transports, registry and reload inheritance |
internal/breaker |
Circuit breaker state machine with an injectable clock |
internal/health |
Active probing and health state transitions |
internal/proxy |
Forwarding, retries, upgrades, streaming, access logs |
internal/ratelimit |
Token bucket limiter with idle bucket collection |
internal/metrics |
Prometheus collectors and a rolling in memory window |
internal/admin |
Admin API, authentication and the embedded dashboard |
internal/server |
Listener lifecycle, atomic configuration swap, graceful shutdown |
A deeper description of the concurrency model, the reload protocol and the design trade offs is in docs/architecture.md.
git clone https://github.com/cansarihan/vane.git
cd vane
make build
./bin/vane -config configs/vane.yaml -check
./bin/vane -config configs/vane.yamlThe demo starts seven sample backends, launches Vane with configs/vane.demo.yaml and generates traffic.
It is the environment used for the screenshots above.
make demoOnce it is running, open http://127.0.0.1:19090 for the dashboard and send traffic through
http://127.0.0.1:18080.
docker build -t vane:local .
docker run --rm -p 8080:8080 -p 9090:9090 \
-v "$PWD/configs/vane.yaml:/etc/vane/vane.yaml:ro" \
vane:localA compose file with three API backends and one web backend is provided:
docker compose -f deploy/docker-compose.yml up --buildVane reads one YAML file. Unknown fields are rejected, every field is validated before the configuration is
applied, and ${ENVIRONMENT_VARIABLES} are expanded while the file is read. Validate a file without starting
the server with vane -check or vanectl check.
server:
listen: ":8080"
read_header_timeout: 5s
read_timeout: 30s
write_timeout: 30s
idle_timeout: 90s
shutdown_timeout: 15s
max_header_bytes: 1048576
trusted_proxies:
- 127.0.0.1
- 10.0.0.0/8| Field | Default | Description |
|---|---|---|
listen |
:8080 |
Plain HTTP listen address |
read_header_timeout |
5s |
Deadline for reading request headers |
read_timeout |
30s |
Deadline for reading the full request |
write_timeout |
30s |
Deadline for writing the response |
idle_timeout |
90s |
Keep alive idle deadline |
shutdown_timeout |
15s |
Grace period for in flight requests during shutdown |
max_header_bytes |
1048576 |
Maximum accepted header size |
trusted_proxies |
empty | Addresses and CIDR blocks whose X-Forwarded-For values are honoured |
upstreams:
- name: api
algorithm: least_connections
targets:
- url: http://10.0.1.10:8080
weight: 3
- url: http://10.0.1.11:8080
weight: 2
health_check:
enabled: true
path: /healthz
interval: 5s
timeout: 2s
healthy_threshold: 2
unhealthy_threshold: 3
expect_status: [200, 204]
circuit_breaker:
enabled: true
failure_threshold: 5
success_threshold: 2
open_duration: 20s
retry:
attempts: 2
backoff: 50ms
on_status: [502, 503, 504]
max_body_size: 1048576
transport:
dial_timeout: 5s
response_header_timeout: 30s
max_idle_conns_per_host: 128
idle_conn_timeout: 90s
insecure_skip_verify: falseroutes:
- name: api-v1
host: api.example.com
path_prefix: /v1
methods: [GET, POST]
upstream: api
strip_prefix: false
timeout: 20s
rate_limit:
enabled: true
requests_per_second: 200
burst: 400
headers:
request_set:
X-Vane-Route: api-v1
request_remove:
- X-Internal-Token
response_set:
X-Edge: vane
response_remove:
- ServerRoutes are evaluated by specificity rather than file order: exact hosts outrank wildcard hosts, wildcard hosts outrank host independent routes, and longer path prefixes outrank shorter ones.
admin:
enabled: true
listen: "127.0.0.1:9090"
token: "${VANE_ADMIN_TOKEN}"
ui: true
metrics: true
logging:
level: info
format: json
access_log: trueWhen token is set, every admin endpoint except /healthz requires Authorization: Bearer <token> or a
token query parameter. The dashboard forwards the query parameter it was opened with.
| Value | Behaviour | Suited to |
|---|---|---|
round_robin |
Even rotation over available targets | Uniform, stateless backends |
weighted_round_robin |
Smooth weighted rotation that avoids bursts to heavy targets | Mixed instance sizes |
least_connections |
Lowest in flight count divided by weight | Long or uneven request durations |
ip_hash |
Stable mapping from client address to target | Session affinity without shared state |
consistent_hash |
Hash ring over the request path with 160 virtual nodes per weight unit | Cache locality across a changing target set |
random |
Weighted random selection | Very large target sets |
Only targets that are healthy, not drained and allowed by their circuit breaker take part in selection.
Health checks. Each pool runs an independent checker. A target is marked unhealthy after
unhealthy_threshold consecutive failed probes and returns to rotation after healthy_threshold
consecutive successes. Transitions are logged and exported as vane_upstream_target_healthy.
Circuit breakers. Failures observed on the request path advance a per target breaker. After
failure_threshold failures the breaker opens and the target is skipped. When open_duration elapses the
breaker moves to half open and admits probes; success_threshold successes close it, and a single failure
reopens it.
Retries. A failed attempt is retried on a different target. Requests with a body are buffered up to
max_body_size; larger bodies disable retries for that request. Non idempotent methods are retried only when
the connection to the upstream was never established, so a request that reached the origin is never sent
twice.
Draining. vanectl drain <upstream> <target> removes a target from selection immediately while in flight
requests finish. Drain state is preserved across configuration reloads and restored with vanectl restore.
Static certificates:
server:
tls:
enabled: true
listen: ":8443"
min_version: "1.2"
cert_file: /etc/vane/tls/server.crt
key_file: /etc/vane/tls/server.keyAutomatic certificates over ACME:
server:
tls:
enabled: true
listen: ":443"
acme:
enabled: true
email: ops@example.com
domains:
- edge.example.com
cache_dir: /var/lib/vane/acme
staging: falseWith ACME enabled the plain HTTP listener serves the HTTP-01 challenge. Certificates are cached on disk and renewed automatically. Static certificates are reloaded on every configuration reload, so rotation does not require a restart.
| Method | Path | Description |
|---|---|---|
GET |
/healthz |
Liveness probe, never authenticated |
GET |
/api/v1/status |
Version, uptime, listeners, target counts, memory and reload counters |
GET |
/api/v1/config |
Active configuration with secrets omitted |
GET |
/api/v1/upstreams |
Pools with per target health, counters and circuit state |
GET |
/api/v1/routes |
Routes in evaluation order |
GET |
/api/v1/metrics/stats?seconds=60 |
Rate, error ratio and latency percentiles |
GET |
/api/v1/metrics/series?seconds=60 |
Per second request and error samples |
POST |
/api/v1/reload |
Reload the configuration file |
POST |
/api/v1/drain |
Drain or restore a target |
GET |
/metrics |
Prometheus exposition |
GET |
/ |
Dashboard |
curl -s http://127.0.0.1:9090/api/v1/status | jq '.uptime, .healthy_targets'
curl -s -X POST http://127.0.0.1:9090/api/v1/drain \
-H 'Content-Type: application/json' \
-d '{"upstream":"api","target":"http://10.0.1.11:8080","drained":true}'vanectl [flags] <command> [arguments]
status show runtime status
upstreams list upstream pools and target health
routes list configured routes
stats show traffic statistics for the last minute
reload reload the configuration from disk
drain <upstream> <target> stop sending new traffic to a target
restore <upstream> <target> return a drained target to rotation
check <file> validate a configuration file
version print the client version
-addr string admin API address, defaults to http://127.0.0.1:9090
-token string admin API bearer token
-timeout duration request timeout, defaults to 5s
-json print raw JSON responses
VANE_ADMIN_ADDR and VANE_ADMIN_TOKEN are read when the corresponding flags are absent. Colour is
disabled automatically when the output is not a terminal or when NO_COLOR is set.
Response headers. Every proxied response carries X-Request-Id, X-Vane-Upstream and X-Vane-Target.
Retried requests also carry X-Vane-Attempts. An inbound X-Request-Id is preserved and forwarded.
Access logs. One structured record per request with the request identifier, method, path, host, client address, route, upstream, target, status, duration and response size. Failed requests include the upstream error.
Metrics.
| Metric | Type | Labels |
|---|---|---|
vane_requests_total |
counter | route, upstream, method, status |
vane_request_duration_seconds |
histogram | route, upstream |
vane_request_retries_total |
counter | route, upstream |
vane_rate_limited_total |
counter | route |
vane_upstream_inflight_requests |
gauge | upstream, target |
vane_upstream_target_healthy |
gauge | upstream, target |
vane_upstream_circuit_state |
gauge | upstream, target |
vane_config_reloads_total |
counter | none |
Go runtime and process collectors are registered alongside the proxy metrics.
[Unit]
Description=Vane reverse proxy
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=vane
Group=vane
ExecStart=/usr/local/bin/vane -config /etc/vane/vane.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=2
LimitNOFILE=65535
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/vane
[Install]
WantedBy=multi-user.targetsystemctl reload vane sends SIGHUP, which reloads the configuration in place. The configuration file is
also watched, so an atomic write to it triggers the same path.
The published image runs as an unprivileged user and expects the configuration at /etc/vane/vane.yaml.
Mount a file over it, or bake a derived image.
Measured on an Apple M5 with ten cores, where the load generator, the proxy and the origin all ran on the same host. Numbers therefore describe relative overhead rather than the throughput of a dedicated deployment.
| Scenario | Requests per second | p50 | p95 | p99 |
|---|---|---|---|---|
| Direct to origin, 50 connections | 141,775 | 0.3 ms | 0.8 ms | 1.5 ms |
| Through Vane, 50 connections | 34,872 | 1.3 ms | 3.1 ms | 4.5 ms |
Selection cost per request, measured with go test -bench:
| Algorithm | Time per selection |
|---|---|
round_robin |
3.1 ns |
consistent_hash |
460 ns |
Reproduce with:
go test -run=^$ -bench=. ./internal/balancer/
hey -z 15s -c 50 http://127.0.0.1:18080/make test # unit and integration tests
make race # tests under the race detector
make cover # coverage summary
make lint # golangci-lint
make build # binaries into bin/
make demo # local demo with sample backends and trafficThe test suite covers configuration validation, every balancing algorithm, the circuit breaker state machine, the rate limiter, route matching, client address extraction, health transitions, the admin API and the proxy itself, including retries, draining, streaming and WebSocket upgrades. Continuous integration runs formatting checks, module tidiness, golangci-lint, tests with the race detector, cross compilation for Linux, macOS and Windows, configuration validation and a container build.
- gRPC aware routing and load balancing
- Response caching with configurable keys and revalidation
- Weighted traffic splitting for canary releases
- OpenTelemetry trace propagation and span export
- Structured audit log for admin API mutations
Released under the MIT License. Copyright (c) 2026 Can Sarıhan.


