Skip to content

feat: Multi-backend load balancing for proxy groups #436

@almeidapaulopt

Description

@almeidapaulopt

Problem

Each proxy currently targets a single container/backend. If a service is scaled horizontally (multiple container replicas), there is no way to distribute traffic across them. The PortConfig struct in internal/model/port.go already stores targets []*url.URL — a slice — but the reverse proxy always uses GetFirstTarget() in internal/proxymanager/port.go, ignoring all other targets.

This makes TSDProxy unsuitable for scaled-out services unless you run a separate load balancer in front (defeating the simplicity goal).

Proposed Solution

Multi-target support with load balancing

Allow multiple target URLs per port with a configurable balancing strategy:

# Docker labels — comma-separated or additional labels
tsdproxy.targets: "container1:3000,container2:3000,container3:3000"

# Or via port label with multiple targets
tsdproxy.port.1: "443/https:3000/http"
tsdproxy.port.1.target: "container2:3000"
tsdproxy.port.1.target: "container3:3000"

Or in the List provider:

proxies:
  myapp:
    ports:
      "443/https":
        targets:
          - "http://container1:3000"
          - "http://container2:3000"
          - "http://container3:3000"
    loadbalance: "roundrobin"

Load balancing strategies

Strategy Description
roundrobin Distributes requests sequentially across targets (default)
leastconn Sends to target with fewest active connections
first Always uses the first healthy target (active/passive failover)
random Random selection

Health-aware routing

When a target is unhealthy (existing health checker detects it), it should be drained from the pool until it recovers. This builds on the existing health check infrastructure in internal/proxymanager/health.go.

Implementation Notes

  • The targetState struct in internal/model/port.go already supports multiple targets — the data model is ready
  • The ReverseProxy in internal/proxymanager/port.go needs a custom Transport or director that selects a target per-request
  • A loadbalance field should be added to PortConfig
  • For HTTP, a custom httputil.ReverseProxy director can select a target per-request
  • For TCP, a connection-level balancer is needed (round-robin across backend connections)
  • UDP is connectionless but per-client affinity could be supported via consistent hashing
  • Health check integration: the existing HealthResult can be extended to track per-target health

Scope

Phase 1: Round-robin for HTTP/HTTPS targets (covers 80% of use cases)
Phase 2: Least-connections + health-aware draining
Phase 3: TCP/UDP load balancing

Alternatives

  • Run a separate load balancer (Traefik, nginx, HAProxy) in front of TSDProxy — adds complexity and another service to manage
  • Use Docker Swarm/K8s built-in load balancing (only works within the orchestrator)
  • Tailscale's own load balancing (not available via tsnet)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions