Skip to main content

Waking Idle Apps at the Proxy Layer: Sablier vs KEDA vs Knative for the 30-App Fleet

13 min readDora NodaDora Noda
Share

Thirty apps on one Hetzner box. Twenty-eight of them haven't seen a request in six hours — but every container is still running, still reserving RAM, still warming CPUs that nothing is using. If you self-host a PaaS for a team that ships preview environments, internal tools, and staging branches, idle is your default state. The question isn't whether scale-to-zero saves money — it's which scale-to-zero you can actually operate at homelab scale without turning the autoscaler into a second platform to maintain.

Sablier answers that question at the layer you already run: the reverse proxy. Where KEDA and Knative bring event-driven operators and serverless control planes into the cluster to watch queues and HTTP concurrency, Sablier is a Traefik, Caddy, Nginx, Envoy, or APISIX plugin that intercepts the first request for an idle workload, starts it, holds the caller just long enough, and lets everything else go idle again. For the fleet with dozens of low-traffic apps — not thousands — that difference in operational surface is the whole argument. The ceiling is real too: a cold start measured in seconds, no queue-depth scaling, and no fine-grained per-message concurrency. This post maps exactly what Sablier buys, what it costs when it wakes, and where you graduate to the heavier tools.


How Sablier Wakes Work at the Edge, Not the Control Plane

Sablier (github.com/acouvreur/sablier) is a small Go service plus a plugin that lives inside your reverse proxy. It knows about your workloads through familiar providers — Docker, Docker Swarm, Podman, Kubernetes Deployments/StatefulSets, and even Proxmox LXC via the community providers — and it keeps a session with a TTL for each one. When traffic arrives for a service that has been scaled to zero, the plugin doesn't 502. It asks Sablier to start the target, then handles the caller with one of two strategies:

  • Blocking: the proxy holds the HTTP request open until the container reports healthy (or a configurable timeout, default 60s). The caller sees normal latency plus startup time — one request, one round trip, no redirect.
  • Dynamic (waiting page): the proxy returns a themable HTML page that auto-refreshes every few seconds (default 5s polling interval) while Sablier starts the backend. The user watches a "waking up" screen; the underlying workloads are identical.

After sessionDuration of no traffic (you set it per-service, e.g. 10m, 30m, 1h), Sablier's reconciler scales the workload back to zero itself — stopping the Docker container or scaling the Kubernetes replicas to 0. No HPA, no custom metrics server, no queue to poll. The only persistent component is the Sablier service (a single binary/container) and the plugin configuration in the proxy.

A minimal Traefik example tells the whole story. You register a middleware that names the workload group and points at Sablier:

yaml
# Traefik dynamic config
http:
  middlewares:
    whoami-sablier:
      plugin:
        sablier:
          sablierUrl: "http://sablier:10000"
          group: "whoami"
          names: "deployment_default_whoami_1"
          strategy: blocking
          blockingTimeout: "45s"

And in Sablier you declare how long an idle session lives:

yaml
# sablier - Docker provider example
services:
  whoami:
    instances:
      - name: whoami
        replicas: 1
    sessionDuration: 10m

The same shape works for Caddy (sablier Caddy module), Nginx (ngx_http_sablier_module), or Envoy as a filter. The proxy already terminates every byte that matters for wake decisions; Sablier just gives the proxy permission to treat replicas=0 as "asleep, not dead."

The Comparison Your 30-App Fleet Actually Needs

If you're running ten to fifty apps on one or two nodes, the relevant question is not theoretical throughput but what new infrastructure each scale-to-zero option drags in. This is the table that should sit on your decision doc:

CapabilitySablierKEDAKnative Serving
Where it livesReverse-proxy plugin + one binaryCluster operator + metrics serverControl plane (activator, autoscaler, queue-proxy sidecar, net-istio/kourier)
What it watchesInbound HTTP at the proxy (per-request intercept)External event sources: queue depth, Kafka lag, cron, Prometheus, HTTP add-onHTTP concurrency/RPS via queue-proxy
TargetsDocker, Swarm, Podman, K8s, Proxmox LXCAny ScaledObject: Deployment, Job, etc.Knative Service revisions
Scales to zeroYes, after inactivity TTLYes, minReplicaCount: 0Yes, minScale: 0 with activation path
Wakes onFirst HTTP request onlyEvent metrics crossing thresholdHTTP request via activator
Cold-start shapeBlocking hold or waiting page, 2–30s typicalPod start + readiness, secondsQueue-proxy + activator + image pull + probes, ~3s–60s reported in benchmarks
Operational extrasNone beyond proxy configCRDs (ScaledObject, TriggerAuthentication), 60+ scalers, scaler auth secretsIstio/Kourier, networking layer, sidecar per pod
Best idle profileDozens of heterogeneous low-traffic web apps, preview envs, staging branchesQueue consumers, workers, bursty pipelines that must react to backlog depthServerless HTTP services that need concurrency autoscaling, traffic splitting, revisions
CeilingOne wake trigger (HTTP), no per-message or per-lag fan-out; not for 1000s of services with distinct SLAsNeeds meaningful event source; overkill if you only have HTTP idlenessHeaviest control plane; cold-start tail + sidecar tax; opinionated networking

For the small fleet the operational delta is the feature. Sablier adds a middleware line. KEDA adds an operator you upgrade, a metrics pipeline you debug, and scaler credentials you rotate. Knative adds a networking layer and a per-pod sidecar that changes your deployment shape (a Knative Service rather than a Deployment + Ingress). All three can leave your cluster at zero pods and — paired with Cluster Autoscaler — even at zero nodes for a pool, but the cost to explain and recover each one when it misbehaves at 2am is not comparable.

What Idle Actually Costs Without and With Scale-to-Zero

Take a concrete, unflattering fleet: 30 apps on a Hetzner CX22-class node (2 vCPU, 4GB RAM, ~€5.83/mo) or two such nodes. Each app is a Node, Python, or Go service averaging 180MB RSS and 0.15 vCPU limit, idle most of the day. Traffic is human-driven: internal dashboards, PR previews, client demos — eight hours of waking use on weekdays, near-zero nights and weekends.

  • Without scale-to-zero: 30 replicas × 180MB ≈ 5.4GB RAM requested before any burst headroom. You need at least two CX22s or a single larger CPX11 (~€10/mo, 2 vCPU/2GB is too little; realistically a CPX21 at ~€15/mo) and you pay for 720 hours per app per month. Idle-hour cost is literal: you're reserving memory for services no one calls.
  • With Sablier at a 20-minute TTL: measurements from homelab operators running Sablier + Traefik consistently report 65–85% idle-hour reclamation on staging/preview fleets. If your 30 apps are collectively touched only 6–8 hours a day on weekdays, Sablier keeps average concurrent replicas around 4–6 during work hours and 0–1 outside them. That's roughly 300–450 container-hours per month instead of 21,600.

In node terms, the fleet collapses to one CX22 most of the day rather than two permanently hot nodes plus headroom. The saving isn't magic — it's RAM you stopped reserving. On a €6–15/mo node, collapsing two nodes into one part-time node reclaims €6–10/mo per redundant box entirely, before you count the preview branch that would otherwise need its own 24/7 replica "just in case someone clicks it tomorrow." Multiply that across preview environments (one per PR) and the idle tax is the dominant line item: every short-lived branch you keep alive 24/7 for a three-day review window bills ~60 idle hours for every hour it was actually used.

Kubernetes makes the same math visible in GiB-hours. A KEDA ScaledObject with cooldownPeriod: 120 and pollingInterval: 15 and a Knative minScale: 0 both leave zero pods and, with Cluster Autoscaler, can leave zero nodes after the scale-down grace period. Sablier's advantage for the small, non-Kubernetes or mixed fleet is that it gets the same idle-hour collapse without standing up the cluster autoscaler's prerequisites: you don't need metric adapters or a custom concurrency metric to justify keeping zero replicas warm.

The Ceilings You Have to Design Around

Sablier's lightness is a tradeoff, not a trick. A few limits shape whether it fits your workload at all:

1. Cold start is user-visible, every time. The first request after idle pays the full start cost: Docker image already present is 1–5s for a small Go/Node image, 5–15s for a heavier Python/Ruby app with migrations, and 15–30s if the container pulls an image layer or waits for a database connection. The blocking strategy holds the TCP connection and most browsers tolerate a 10–30s Time-To-First-Byte — but API clients, webhooks, and curl-style health checks often do not. The dynamic/waiting-page strategy avoids the long TTFB but introduces a second user-visible step. For an internal tool or a PR preview that's fine; for a public API with a 500ms SLA on every request it is disqualifying. KEDA and Knative have the same physics — Knative benchmarks on kind clusters report ~3.2s cold start for a trivial image and 30–60s for larger ones including the activator/queue-proxy path — but they hide it behind retry and concurrency knobs you tune per service rather than a single blocking timeout.

2. One trigger: HTTP at the proxy. Sablier wakes on inbound HTTP and only inbound HTTP. It does not watch Kafka consumer lag, SQS queue depth, cron schedules, or Prometheus metrics to pre-warm a worker. If your scale-to-zero need is a queue consumer that must wake because its topic has 10,000 messages — not because someone fetched / — Sablier has no opinion. That's exactly KEDA's home turf: a kafka or aws-sqs-queue scaler that grows pods as backlog grows and drains them when it clears, with zero HTTP ever involved. Likewise, a service that must fan out by message count rather than hold one request needs KEDA, not a proxy plugin.

3. No per-pod concurrency control. Sablier starts a workload group as declared; it does not add a second replica because the first is at 80% concurrency. Knative's autoscaler does (target: 100 concurrent requests per pod, scale to ceil(inflight / target)), and KEDA's HTTP add-on or Prometheus scaler can approximate it. If your idle app is bursty within a single session — a webhook endpoint that goes from 1 to 500 RPS in a minute — Sablier wakes one instance and leaves you to handle burst with a horizontal autoscaler behind it. Operators often pair Sablier for the 0→1 transition with a standard HPA for 1→N, but that's two systems with two polling intervals to tune.

4. Diminishing returns past a few hundred services. Sablier's session store is per-Sablier-instance memory with file persistence optional. It works cleanly for dozens of services and for hundreds with careful grouping, but it has no cross-region reconciliation, no per-tenant quota, and no notion of "warm pool" or predictive pre-warming. Knative's revision/traffic-splitting and KEDA's scaler-per-service model are built for platform teams that need per-team budgets and per-service SLOs. If you're operating at that level, Sablier saves you nothing on the organizational overhead you already have.

Operating Sablier Without Surprises

A few settings turn a working Sablier setup into a boring one:

  • Pick the strategy per workload, not globally. Internal dashboards and PR previews where a user will tolerate a short wait are good candidates for blocking with a 30–60s timeout. Customer-facing marketing pages where the first impression matters should stay on a real minReplicas: 1 (or a longer Sablier TTL) rather than forcing every cold visitor through a wake sequence.
  • Set sessionDuration from real traffic, not a guess. Export your proxy access logs, bucket requests into 5-minute windows, and measure gaps: a 20m TTL that matches a typical meeting-length pause feels snappy; a 2m TTL that forces a cold start for every coffee break feels broken. Supervision start with 10–20m for dashboards, 30–60m for preview branches that people context-switch away from.
  • Make readiness real. Sablier marks a workload ready when Kubernetes reports replicas == desired or Docker reports the container healthy. If your readiness probe returns 200 before the app can actually answer — because migrations haven't run or a connection pool hasn't primed — the first proxied request still fails. Gate readiness on the same path the real user will hit, not just TCP:80.
  • Pair with health-aware routing for APIs. For API consumers that cannot tolerate blocking, put a tiny error middleware in front of Sablier that returns 503 Retry-After: 15 on the very first probing request, and let a well-behaved client retry once. It's one more layer, but it removes the 30s hanging-request trap for non-browser callers.
  • Log and alert on cold starts, not just idleness. The metric that matters is cold_starts_per_service_per_day × p95_wake_latency. If a service wakes 200 times a day and each wake costs 12s, you've built a slow service, not an efficient one. Promote it to minReplicas: 1.

For a Docker or Podman homelab, the quickest proof is one proxy, one Sablier container, and one idle service — no Kubernetes required. For a small k3s or k3d cluster backing bex-style git-push workloads, the Kubernetes provider watches Deployments directly and the same Traefik Middleware or Caddy handle block routes the wake.

Where to Graduate

The honest lifecycle for a small PaaS looks like this:

  • Sablier alone when your scale problem is HTTP idleness across a heterogeneous, low-traffic estate and your infra team is one or two people who'd rather maintain a proxy config than an operator matrix. That's the homelab, the early-stage startup with 40 services and two SREs, the consultancy that spins a preview per branch.
  • Sablier + HPA/KEDA when the estate grows a queue layer underneath the HTTP story — background workers, webhook processors, or ML inference endpoints that must wake on queueDepth or consumerLag rather than GET /. Let Sablier handle the 0→1 for web shells, let KEDA own the worker pool.
  • Knative (or KEDA HTTP add-on) as the platform when per-service concurrency, traffic splitting, and revision pinning become product requirements rather than incidental polish — the point where "which idle service woke" is auditable and billable, not just observable.

None of these erase cold starts. They just move where you account for them. A pod still needs an image, a scheduler decision, and a probe. The difference is whether that latency shows up as a blocking request, a metrics-driven scale-up, or an activator's retry — and how many components you debug when the answer is wrong.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. When your fleet's idle hours outnumber its busy ones, waking containers at the proxy layer is one of the cheapest capacity wins you can ship first — before you stand up an event-driven autoscaler you don't yet need. Star the repo on GitHub and try scale-to-zero on hardware you already picked.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex