Scale to Zero
Preview environments are incredibly powerful, but they can be expensive if they run 24/7. Most pull requests are reviewed intermittently—a few minutes of activity followed by hours or days of idle time.
Diverge solves this by allowing your preview environments to scale to zero when they aren’t receiving traffic.
Why Scale-to-Zero Matters
Section titled “Why Scale-to-Zero Matters”- Massive Cost Savings: 50 PRs with always-on pods cost ~$1,200/month. With Knative scale-to-zero, that same workload costs ~$15/month (illustrative estimate based on 50 always-on pods vs. ~3 concurrently active).
- Resource Efficiency: Fit hundreds of preview environments on a small cluster.
- Fast Startup: Previews wake up automatically when a reviewer opens them (startup is typically ~5 seconds, but varies with image size, scheduling, and readiness probe configuration). The Activator buffers requests during startup, though requests may time out if pod startup exceeds the client or proxy request timeout.
How It Works
Section titled “How It Works”Diverge integrates with Knative Serving and its own Activator Proxy to manage the lifecycle of your pods:
- Idle Detection: Knative monitors traffic to your preview services. When no requests arrive for a configurable period, Knative scales the deployment down to 0 replicas.
- Wake Up Flow: When a new request arrives, it hits the Activator Proxy.
- Queue & Scale: The proxy holds the request, triggers a scale-up of the service to 1 (or more) replicas, and waits for the pod to become ready.
- Forward: Once ready, the proxy forwards the queued request to the newly running pod. Subsequent requests go directly to the pod.
The Activator Proxy
Section titled “The Activator Proxy”The Activator Proxy (activator-proxy) is a specialized binary that sits in front of preview environments.
- Smart Routing: Routes to ready pods directly when available to minimize latency.
- Fallback to Knative / Scale-Up: If pods are scaled to zero, it triggers a scale-up of the deployment (or falls back to the Knative/KEDA activator) to wake the service.
- Preview Wake-Up Splash Page (
--wake-splash): Serves an auto-refreshing202 AcceptedHTML splash screen to browsers during cold starts instead of hanging the browser tab, while keeping API/programmatic clients buffered transparently. - Header Injection: Injects the
X-Preview-Envheader to ensure routing rules continue to work. - Efficient State Tracking: Uses a shared informer to efficiently track pod states across the cluster.
Preview Wake-Up Splash Page (--wake-splash)
Section titled “Preview Wake-Up Splash Page (--wake-splash)”When a reviewer clicks a preview URL in a browser while the preview pod is scaled to zero, waiting several seconds on a blank loading spinner can feel broken.
The activator-proxy supports a configurable Wake-Up Splash Page (controlled globally via --wake-splash, default false, or per request via the X-Diverge-Wake-Splash: true header):
- Browser Detection: When all pods for the target preview environment are scaled to zero (
0ready pods) and the incoming request is a browser navigation (GETorHEADwithAccept: text/html),activator-proxytriggers the background scale-up and immediately returns202 AcceptedwithRetry-After: 2. - Auto-Refreshing Splash Screen:
GETrequests receive a lightweight HTML splash page (“Waking up preview environment…”) that pollsGET /-/wakeup-statusevery 1.5 seconds and automatically reloads the page once the pod passes readiness probes, whileHEADrequests receive only the status and headers with no body. - Readiness Endpoint (
GET /-/wakeup-status): Returns JSON{"ready": true|false}so both the splash screen and external health checkers can inspect cold-start progress without triggering additional upstream proxies. - API & CLI Transparency: Non-HTML requests (
POST,PUT,application/json, gRPC, orcurlwithoutAccept: text/html) bypass the splash screen and block until the pod is ready before forwarding the request.
Idle Hibernation Annotations
Section titled “Idle Hibernation Annotations”Even without Knative or KEDA installed, Diverge’s EnvironmentReconciler supports native Idle Hibernation for standard Kubernetes deployments using two annotations on the Environment CR:
| Annotation | Example | Description |
|---|---|---|
divergedev.com/hibernate-after |
"30m" |
Duration of inactivity after which the controller scales managed preview Deployments and StatefulSets to 0 replicas and sets divergedev.com/hibernated: "true" (Hibernated condition). |
divergedev.com/last-traffic-at |
"2026-10-12T01:15:00Z" |
RFC3339 timestamp of the last observed HTTP/gRPC request (updated by diverge-proxy / activator-proxy). If omitted, inactivity is measured from status.createdAt. |
When traffic arrives again or divergedev.com/hibernated is cleared, the controller wakes the hibernated environment back to Running.
Namespace Quotas & FinOps LRU Auto-Hibernation
Section titled “Namespace Quotas & FinOps LRU Auto-Hibernation”Platform teams can enforce hard capacity and FinOps budget guardrails per Kubernetes Namespace using annotations on the Namespace resource:
| Namespace Annotation | Example | Description |
|---|---|---|
divergedev.com/max-active-environments |
"10" |
Maximum concurrent non-hibernated preview Environments in the namespace |
divergedev.com/max-cnpg-clusters |
"3" |
Maximum concurrent dedicated CloudNativePG (cnpg/fresh/snapshot) database clusters |
divergedev.com/max-hourly-cost-usd |
"2.50" |
Maximum aggregate hourly FinOps burn rate across active Environments in the namespace |
divergedev.com/quota-exceed-policy |
"hibernate-oldest" |
"hibernate-oldest" (default — scales the least-recently-used active Environments to 0 replicas to free budget) or "reject" (marks new environments Failed with QuotaReady=False / Reason: QuotaExceeded) |
Sequence Diagram
Section titled “Sequence Diagram”Here is what happens when a user accesses a scaled-to-zero preview environment:
sequenceDiagram
actor Developer
participant Proxy as Activator Proxy
participant Knative as Knative / K8s Scale
participant K8s as Kubernetes API
participant Pod as Preview Pod
Developer->>Proxy: GET / (Accept: text/html, Header: X-Preview-Env)
Proxy->>K8s: Check Ready Pods
K8s-->>Proxy: 0 Pods Ready
Proxy->>Knative: Trigger Scale Deployment to 1
Proxy-->>Developer: 202 Accepted (Wake-Up Splash HTML)
K8s->>Pod: Start Container
Developer->>Proxy: GET /-/wakeup-status?env=pr-42
Pod-->>K8s: Pod Ready
Proxy-->>Developer: {"ready": true, "readyPods": 1}
Note over Developer, Pod: Browser Auto-Reloads & Subsequent Requests
Developer->>Proxy: GET /
Proxy->>K8s: Check Ready Pods
K8s-->>Proxy: 1 Pod Ready
Proxy->>Pod: Direct Forward (Bypass Cold-Start Path)
Pod-->>Developer: 200 OK
Configuration
Section titled “Configuration”Diverge supports three scale-to-zero and deployment mechanisms:
- Annotation-based Idle Hibernation (
divergedev.com/hibernate-after): Works with standard Kubernetes Deployments (--deploy-provider=direct) without extra CRDs. - KEDA HTTP Add-on (
spec.serviceConfig.keda.enabled: true): Reconciles a KEDAHTTPScaledObject(andScaledObjectfor Kafka/Temporal workers) perEnvironment. - Knative Serving (
--deploy-provider=knative): Reconciles KnativeServiceresources that scale to zero natively.
Configure KEDA HTTP scale-to-zero and idle hibernation on an Environment via spec.serviceConfig.keda:
apiVersion: divergedev.com/v1alpha1kind: Environmentmetadata: name: payments-pr-42 annotations: divergedev.com/hibernate-after: "30m"spec: serviceConfig: serviceName: payments-api image: ghcr.io/acme/payments:pr-42 port: 8080 keda: enabled: true cooldownPeriod: 300 minReplicas: 0 maxReplicas: 5