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:

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:

  1. Checks hash ring ownership -- if this controller is no longer responsible, the supervisor skips scaling but continues tracking recommendations.
  2. Collects metrics -- CPU, memory, and in-flight request counts from router heartbeats.
  3. Computes a scaling recommendation using an HPA-inspired algorithm that evaluates each metric independently and takes the highest recommendation.
  4. Applies a stabilization window for downscale decisions to avoid flapping.
  5. Assigns or terminates pods to reach the desired count, respecting min_instances and max_instances bounds.

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:

  1. Parse the x-skipper-function header to identify the target function.
  2. Call the controller's GetInstance RPC to obtain an instance address. The controller either returns an already-assigned pod or assigns a new one from the deployment's unassigned pool.
  3. Proxy the request to the instance using a reverse proxy with configurable retry logic (up to --max-round-trip-attempts with exponential backoff).
  4. 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.
  5. For oneshot functions, call ReleaseInstance after 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