Skip to content

Architecture

Overview

flowchart LR
    Client([Client]) -->|HTTP / HTTPS| Proxy

    subgraph pod[Coxswain]
        direction TB
        Controller -->|atomic swap| RT[(Routing\nTable)]
        Controller -->|atomic swap| TLS[(TLS\nStore)]
        RT -->|atomic read| Proxy
        TLS -->|SNI handshake| Proxy
    end

    K8s[Kubernetes\nAPI Server] -->|watch events| Controller
    Controller -->|status writes\nleader only| K8s
    Proxy -->|forward| Upstream([Upstream\nPods])

The controller watches Kubernetes objects and maintains the routing and TLS tables. The proxy reads from those tables on every request via a single atomic load — no locks, no channels. Routing and TLS updates take effect on the next request after the swap, with no restart and no dropped connections.

Multi-replica and leader election

All replicas reconcile watch events and maintain their own routing table independently. They all serve traffic all the time. What leader election controls is narrower: only status writes (the conditions written back to Ingress, Gateway, and HTTPRoute objects).

flowchart LR
    K8s[Kubernetes API]

    subgraph A[Replica A — leader]
        direction TB
        CA[Controller] --> RTA[(Routing Table)]
        RTA --> PA[Proxy]
    end

    subgraph B[Replica B]
        direction TB
        CB[Controller] --> RTB[(Routing Table)]
        RTB --> PB[Proxy]
    end

    K8s -->|watch| CA
    K8s -->|watch| CB
    CA -->|status writes| K8s

    Clients([Clients]) --> PA
    Clients --> PB

The leader is determined by a Kubernetes Lease in coxswain-system. When the leader is lost, status writes pause for up to one lease TTL (default 15 s) while the new leader is elected. Traffic continues uninterrupted on all replicas during the transition.

TLS hot-reload

Coxswain watches all kubernetes.io/tls Secrets. When a Secret is created, updated, or deleted — including automatic renewals by cert-manager — the TLS store is rebuilt and swapped atomically. New connections immediately use the new certificate; connections already in progress complete with the old one. No restart is required.

Request path

flowchart LR
    A([TCP\nconnection]) --> B{HTTPS?}
    B -->|yes| C[SNI handshake\nselects certificate]
    B -->|no| D
    C --> D[Read host,\npath and query]
    D --> E[Load routing\ntable snapshot]
    E --> F[Host + rule\nmatching]
    F -->|no match| G([404 / 503])
    F -->|match| H[Round-robin\nupstream pick]
    H --> I([Forward request\nreturn response])

Readiness

/readyz returns 200 once every subsystem reports Ready or Degraded. On a freshly started pod this requires:

  • All Kubernetes reflectors have completed their initial list — CRDs must be installed and RBAC must be correct.
  • The routing table has been built at least once.

Subsystems have four states: Pending (still initialising — keeps /readyz at 503), Ready, Degraded (partial fault but still serving), and Failed. Ready and Degraded both pass the readiness gate; Pending and Failed block it.

If /readyz returns 503 on a running pod, inspect /status to see which subsystem is blocked.