class

RinhaDeBackend::Lb

Inherits Reference < Object

Legacy round-robin TCP→UDS load balancer.

The production LB is now HAProxy (see haproxy.cfg and the lb service in docker-compose.yml). This Crystal LB is kept in the tree for reference / fallback and is still built into the runtime image as /usr/local/bin/rinha_lb, but no compose service invokes it by default.

Behaviour (preserved as documented for the historical iteration): the challenge rules forbid the LB from inspecting payload or running business logic, so this is a strict bidirectional byte copy between the client (TCP) and the picked upstream (UDS), with round-robin upstream selection at connection-accept time.

Original design rationale (vs. the older nginx LB):

  1. UDS skips the TCP/IP stack on the LB→API hop entirely (no port allocation, no Nagle, no port reuse pressure under k6 storms).
  2. The previous nginx allowance was paying for an HTTP-aware proxy with logging/templates/configuration parsing. The static Crystal binary is ~3 MB and runs on tens of KB of working set per connection. HAProxy (in mode tcp) keeps both wins.

Concurrency model

Built on Fiber::ExecutionContext::Parallel (Crystal 1.20 stdlib, https://crystal-lang.org/api/1.20.0/Fiber/ExecutionContext/Parallel.html). All fibers — accept loop, c2s and s2c forwarders — live in the same parallel context, so they can be resumed by any of N scheduler threads. Even at the 0.10 CPU cgroup ceiling assigned to the LB, the parallel context lets a sibling fiber make progress while a peer is parked inside an epoll_wait — which is essentially the whole LB workload.

Round-robin selection uses an Atomic(UInt32) counter; with N parallel schedulers we'd race a plain integer. Atomic increment is one lock xadd on x86-64.

Lifecycle of one downstream connection

  1. accept_loop fiber accepts a TCP socket.
  2. It picks the next upstream UDS path (@rr.add(1) % N) and opens a new UNIXSocket — one upstream connection per downstream connection.
  3. Spawns a c2s fiber (downstream→upstream copy). The current fiber drives the s2c copy itself; when either side closes, both sockets are closed and both copy fibers unwind.

Constants

BUF_SIZE = 16384

16 KiB per direction, stack-allocated. The Rinha hot path request/response pair is well under 1 KiB, so most copy() calls do a single read+write. The headroom matters for the body of a slow client where the kernel may hand us partial reads — we still complete each loop iteration in a single syscall pair.

DEFAULT_HOST = "0.0.0.0"
DEFAULT_PARALLELISM = 2
DEFAULT_PORT = 9999

Constructors

new(host : String, port : Int32, upstreams : Array(String), parallelism : Int32 = DEFAULT_PARALLELISM)
Source

Instance methods

listen
Source