Skip to content

Repository files navigation

Vane

CI CodeQL Go report card Go 1.25 MIT

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.

Contents

Capabilities

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

Screenshots

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.

Vane control plane

vanectl exposes the same information for terminals and scripts.

vanectl

Every request is traceable through response headers, Prometheus counters and the structured access log.

Observability

Architecture

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
Loading

Request lifecycle

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
Loading

Package layout

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.

Quick start

Build from source

git clone https://github.com/cansarihan/vane.git
cd vane
make build
./bin/vane -config configs/vane.yaml -check
./bin/vane -config configs/vane.yaml

Run the local demo

The 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 demo

Once it is running, open http://127.0.0.1:19090 for the dashboard and send traffic through http://127.0.0.1:18080.

Run with Docker

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:local

A compose file with three API backends and one web backend is provided:

docker compose -f deploy/docker-compose.yml up --build

Configuration

Vane 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

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

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: false

Routes

routes:
  - 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:
        - Server

Routes 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 and logging

admin:
  enabled: true
  listen: "127.0.0.1:9090"
  token: "${VANE_ADMIN_TOKEN}"
  ui: true
  metrics: true

logging:
  level: info
  format: json
  access_log: true

When 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.

Load balancing algorithms

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.

Resilience

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.

TLS

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.key

Automatic 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: false

With 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.

Admin API

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}'

Command line client

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.

Observability

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.

Deployment

systemd

[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.target

systemctl 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.

Container

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.

Performance

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/

Development

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 traffic

The 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.

Roadmap

  • 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

License

Released under the MIT License. Copyright (c) 2026 Can Sarıhan.

About

Layer 7 reverse proxy and load balancer with health checks, circuit breakers, hot configuration reload and a built in control plane

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages