Architecture overview
Skipper has two main components that run inside Kubernetes -- a controller that manages pod lifecycle and a router that proxies HTTP traffic. They communicate over gRPC so that the router can request instance assignments and send heartbeats.
Clients
│
▼
Router (HTTP) ──gRPC──▶ Controller ──▶ Kubernetes API
│ │
▼ ▼
Pod (instance) Pod lifecycle
(assign, scale, delete)
Hash ring and controller replicas
Multiple controller replicas form a hash ring (internal/hashring/). Each controller pod adds its IP to the ring on startup. When a function needs to be managed, its FunctionHash is looked up against the ring to determine which controller replica is responsible.
The ring uses 1024 virtual nodes per IP for even distribution. If a router sends a scale request to the wrong controller, that controller forwards the request to the responsible replica over gRPC.
Informer-driven pod management
The controller uses Kubernetes shared informers to watch for changes rather than polling. Two informer sets drive the system:
- Controller pod informer -- watches pods in the controller's own namespace. Add and delete events update the hash ring, which redistributes function ownership across replicas.
- Function namespace informers -- one per configured namespace. Pod events (add, update, delete) are recorded for lag measurement and trigger informer cache updates.
Supervisors are not created directly from pod events. Instead, the controller discovers supervisors on a timer loop and creates them lazily when GetInstance or Heartbeat RPCs reference a new function.
Supervisors
Each active function gets its own Supervisor (internal/controller/supervisor.go) -- a goroutine with an independent converge loop. Supervisors are stored in a concurrent map keyed by FunctionHash.
A supervisor's converge loop runs on a timer (--scale-interval, default 15s) and on each tick:
- Checks hash ring ownership -- if this controller is no longer responsible, the supervisor skips scaling but continues tracking recommendations.
- Collects metrics -- CPU, memory, and in-flight request counts from router heartbeats.
- Computes a scaling recommendation using an HPA-inspired algorithm that evaluates each metric independently and takes the highest recommendation.
- Applies a stabilization window for downscale decisions to avoid flapping.
- Assigns or terminates pods to reach the desired count, respecting
min_instancesandmax_instancesbounds.
Supervisors are created lazily when a pod event or scale request references a function, and stop themselves when the function has no instances and no heartbeats.
Router request flow
The router (internal/router/) receives HTTP requests and proxies them to function instances:
- Parse the
x-skipper-functionheader to identify the target function. - Call the controller's
GetInstanceRPC to obtain an instance address. The controller either returns an already-assigned pod or assigns a new one from the deployment's unassigned pool. - Proxy the request to the instance using a reverse proxy with configurable retry logic (up to
--max-round-trip-attemptswith exponential backoff). - While the request is in flight, send periodic heartbeats to the controller (every
--heartbeat-interval). Heartbeats prevent the controller from scaling the function to zero. - For oneshot functions, call
ReleaseInstanceafter the response completes to delete the single-use pod.
Key packages
| Package | Purpose |
|---|---|
internal/controller/ |
Kubernetes controller: informers, supervisors, scaling, pod assignment |
internal/router/ |
HTTP reverse proxy, heartbeat sender, gRPC client |
internal/skipper/ |
Core domain types: Function, Instance, Heartbeat |
internal/hashring/ |
Consistent hashing with virtual nodes for controller distribution |
internal/config/ |
Declarative flag binding via struct tags |
internal/telemetry/ |
OTLP tracing and Prometheus metrics |
internal/key/ |
Type-safe structured logging and tracing attributes |