Home · ASPLOS 2024 · Datacenter, Cloud & Networking

RainbowCake: Mitigating Cold-starts in Serverless with Layer-wise Container Caching and Sharing

Hanfei Yu, Rohan Basu Roy, Christian Fontenot, Devesh Tiwari, Jian Li, Hong Zhang and 2 others

Conference
ASPLOS 2024
Published
2024
Pages
335-350
Citations
63

Serverless platforms either cache whole containers (fast but wasteful) or share over-packed ones (frugal but slow) — RainbowCake peels containers into three layers, letting an idle container step down from function-specific to language-shared to universal instead of dying, so the next invocation from anyone starts partway warm.

The argument in one glance

  1. Problem

    The cold start is as long as the work

    Serverless functions run for seconds or less, but preparing their container — environment, language runtime, user libraries — takes hundreds of milliseconds. Worse, invocations are bursty and hard to predict: in Azure production traces, over half of all functions show significantly varying arrival patterns, so any prediction-based warm-up will often be wrong.

  2. What people did

    Cache full containers, cache partial ones, or share them

    Full-container caching (keep-alive and pre-warming, as in Azure’s histogram policy or FaaSCache) keeps ready-to-run containers idle in memory. Partial caching (SEUSS) snapshots containers mid-initialization to save memory. Sharing schemes (Pagurus, SOCK) pack several functions’ dependencies into one container so an idle container can serve neighbors.

  3. Why it fell short

    Each approach fixes one half of the trade-off

    A fully initialized container is fully specialized: one function, hundreds of megabytes, wasted on any misprediction. Partial caching cuts the memory but delivers only partial warm starts — the latency win is halfway. Sharing tolerates mispredictions but its over-packed monolithic containers bloat memory for everyone. No prior design gets both low latency and low waste.

  4. This paper's idea

    Cache by layer, share by layer, downgrade instead of kill

    Container initialization has three natural stages — Bare (environment, any function), Lang (language runtime, same-language functions), User (one function’s code). RainbowCake keeps idle containers alive by stepping them DOWN this ladder as timeouts expire, with per-layer time-to-live values computed from sharing-aware arrival models. On OpenWhisk with Azure traces, it cuts function startup latency 68% and container memory waste 77% versus state-of-the-art baselines.

Why this problem is worth solving

Serverless computing’s promise is that users upload a function and the platform handles everything else — provisioning, scaling, billing per invocation. The catch is hiding in “everything else”: each invocation needs a

an isolated virtual environment (a Docker container or Firecracker microVM) holding the function’s runtime and code
, and building one from scratch takes hundreds of milliseconds — the same order of magnitude as typical function execution itself. This is the cold start problem, and it lands exactly where it hurts: on latency-sensitive, sub-second workloads.

If arrivals were predictable, platforms could simply warm containers just in time. They are not. The Azure Functions production study the paper builds on found that over 50% of functions have significantly varying invocation patterns; large fractions are invoked rarely (once an hour) yet still expect fast starts. So every caching policy lives with mispredictions — and a mispredicted full container is a double loss: the invocation cold-starts anyway, and hundreds of megabytes sat idle for nothing. At platform scale, idle cached memory is a direct cost; the paper measures baselines wasting several times more memory-seconds than necessary. This site’s serverless explainers (IceBreaker, AQUATOPE, INFless) all orbit this same prediction problem; RainbowCake’s angle is to make the cache entries themselves degrade gracefully when predictions fail.

What people did before, and where it broke

Full-container caching. The industry default: keep a finished container alive for a fixed window (OpenWhisk and commercial platforms use ~10 minutes), or predict per-function inter-arrival times and schedule pre-warming and keep-alive around them (the Azure Histogram policy), or manage the pool greedily like an object cache (FaaSCache). All variants share a weakness: a finished container is specialized — it serves exactly one function. Wrong function, wrong time → cold start plus wasted memory.

Partial-container caching. SEUSS and similar systems observed that initialization has stages, and cached snapshots partway through — much lighter in memory. But an invocation hitting a partial container still pays the remaining initialization: halfway cold-start mitigation.

Container sharing. Pagurus and relatives recycle idle containers across functions by packing multiple functions’ dependencies into one monolithic image. Sharing tolerates mispredictions (someone else’s container can serve you), but the over-packed containers are heavy, so the same memory budget holds fewer of them — excessive memory waste in exchange for generality.

The paper’s motivating experiment runs all three philosophies on the same 8-hour production trace: partial caching wins on memory but loses on latency; sharing helps latency but bloats memory; full caching sits in the uncomfortable middle. The two fine-grained ideas were never combined — that combination is RainbowCake.

RainbowCake in an OpenWhisk worker

  1. History Recorder new

    Watches invocation arrivals per function; over a sliding window of the latest 6 invocations it fits a Poisson arrival model, then compounds models per layer: a Lang(Python) container’s expected traffic is the sum over ALL Python functions; a Bare container’s over all functions.

  2. Container Pool (layer-wise) new

    Maintains containers in three states — Bare (environment only, serves anyone), Lang (plus language runtime, serves same-language functions), User (plus user package, serves one function) — instead of the traditional alive-or-dead binary.

  3. Event-driven pre-warming new

    On each invocation arrival, estimates the function’s next inter-arrival time (at quantile p = 0.8) and schedules one User-container pre-warm for that moment — skipped if an idle User container already exists. Constant work per event; no global optimization.

  4. Event-driven keep-alive (downgrade ladder) new

    When an idle container’s TTL expires, it is not killed but DOWNGRADED: User→Lang wipes the user package (a Clean HTTP request removes the Action folder), Lang→Bare removes the runtime, Bare→terminated. Each level gets a fresh TTL from its own sharing-aware model, capped so idle cost can never exceed the startup cost it could save.

  5. OpenWhisk container proxies

    Standard OpenWhisk machinery — Akka actor per container with a finite state machine — extended with the new states and HTTP handlers. 8K lines of Scala, open-sourced.

The exact optimization (which layers to hold, for how long, per function) is an NP-complete integer program; RainbowCake replaces it with constant-time event-driven decisions that inherit the structure from the layer models.

The idea

Initialization has natural seams — cut there. A cold start runs three stages: environment setup (network, logging, health checks), language runtime initialization (pull and start Python/Node.js/Java), and user package loading (code, libraries, environment variables). RainbowCake freezes containers at these boundaries as Bare, Lang, and User containers. The seams are real system boundaries — nothing is broken by stopping between them — and they produce a clean gradient: lower layers are lighter and more shareable; higher layers are heavier and save more latency. A Bare container serves any function but leaves the most work to do; a User container starts instantly but serves exactly one function.

Downgrade, don’t kill. Traditional keep-alive is binary: a container idles at full readiness, then dies. RainbowCake’s containers descend a ladder instead. When a User container’s time-to-live (TTL) expires, the platform wipes the user package (and only that) — leaving a Lang container that any same-language function can claim. When that TTL expires, the runtime is removed, leaving a universal Bare container. Only after the third timeout does the container terminate. Each downgrade releases most of the memory-liability while preserving the layers most likely to be reused — the misprediction penalty becomes graduated rather than total.

Model the sharing, not just the function. Here is the quantitative heart. For each function, the History Recorder fits a Poisson arrival model over its recent invocations. But a Lang(Python) container is not waiting for one function — it can be hit by any Python function, so its relevant arrival process is the compound of all Python functions’ models (rates add). A Bare container compounds the entire workload. This is why lower layers earn their keep: their aggregated hit rates are far higher than any single function’s, which a function-specific predictor would never see.

Cap every TTL by an economic argument. Keep-alive length per layer is the predicted time-to-next-hit (at quantile p = 0.8), but never more than a bound β = α·t̄ / ((1−α)·m̄), where t̄ is the layer’s startup saving, m̄ its memory footprint, and α the platform’s latency-vs-memory knob (0.996 in the evaluation). The bound enforces a simple invariant: a container may never burn more idle-memory cost than the startup cost it could possibly save. Past that point, holding it is a guaranteed loss regardless of predictions.

Wipe before sharing. Sharing raises the obvious security question. Downgrades are engineered to be forgetful: functions run as non-root inside a jailed Action folder; the User→Lang transition deletes that folder and unsets user environment variables via a dedicated Clean handler, and user-spawned processes are tracked by PID. Lang and Bare containers are guaranteed free of user remnants before anyone else touches them.

One container's afternoon under RainbowCake

IR-Py = an image-recognition function in Python; DV-Py = a different Python function. TTL = keep-alive time-to-live per layer, TTL = min(predicted inter-arrival at quantile 0.8, cap β). The Clean request wipes the user's Action folder. Latency shares are typical of the paper's Fig. 14 breakdown; the trace is illustrative.

Step 1 of 6

Cold start: climb all three layers

IR-Py is invoked with no warm container anywhere. The platform builds Bare (environment, proxies, networking), upgrades to Lang by initializing the Python runtime, upgrades to User by loading IR-Py’s code and libraries, then runs. Each stage’s cost varies per function; transitions between layers add less than 3% overhead.

Bare → Lang → User → run
cold start: hundreds of ms, comparable to execution

Why it works

  • Initialization stages are real system seams → freezing at Bare/Lang/ User breaks no dependencies → partial containers are always safely resumable, and upgrades install only what is missing.
  • Shareability and cost are inversely stacked → the cheap-to-keep layers are exactly the widely-reusable ones → memory spent on idle containers concentrates where hit probability is highest.
  • Arrival rates add across functions → a Lang container’s compound hit rate dwarfs any single function’s → lower layers are statistically worth keeping even when each individual function is unpredictable — bursts from any same-language function can land on them.
  • The β cap bounds every container’s idle cost by its potential saving → even a fully wrong prediction loses at most what a warm start would have won → memory waste is structurally bounded, not merely hoped away.
  • Downgrades convert mispredictions from total losses into partial ones → the system degrades gracefully exactly when predictors fail (bursty traces) → which is where the baselines fall apart.

What it costs

  • A small latency concession to the greediest baseline. FaaSCache — which never terminates containers until eviction — beats RainbowCake by about 50 ms average (20 ms startup); RainbowCake repays that with 75% less memory waste.
  • Residual partial cold starts. Lang and Bare hits still pay the missing layers’ initialization; only User hits are instant.
  • Modeling assumptions. Poisson arrivals over a 6-invocation sliding window, quantile and knob parameters (p = 0.8, α = 0.996) tuned by sensitivity study — different platforms may need retuning.
  • Trust in the wipe. Cross-function sharing of Lang/Bare containers rests on the Clean handler, non-root execution, and jailing removing every user remnant; the isolation argument is engineering discipline, not a new formal boundary.
  • Transition machinery. Downgrade/upgrade HTTP handlers and FSM states add moving parts, though measured at under 3% of startup latency.

The evaluation

RainbowCake is implemented in Apache OpenWhisk (8K lines of Scala, open source) and evaluated on a three-node AWS EC2 cluster whose 96-core, 240 GB worker runs the containers. The workload: 20 real functions across Node.js, Python, and Java from three benchmark suites (web, data analytics, ML inference, scientific, video), driven by real Azure Functions production traces — one 8-hour set for overall results plus seven 1-hour sets with burstiness (inter-arrival coefficient of variation) swept from 0.2 to 4.0.

Five cold-start philosophies on identical hardware and traces

PropertyFull caching (Histogram / FaaSCache)Partial caching (SEUSS)Sharing (Pagurus)RainbowCake
What idles in memory
Average end-to-end / startup latency (20 functions, 8 h trace)
Total memory waste (idle GB × seconds)
Under burstiness (IAT CV 0.2 → 4.0) and tight memory budgets
Three-node AWS EC2 OpenWhisk cluster (m5.24xlarge worker: 96 cores, 240 GB); 20 functions in Node.js/Python/Java from SeBS/FunctionBench-class suites; real Azure Functions traces (8-hour main set; seven 1-hour sets at CV 0.2–4.0, 3,600 invocations each). Ablations: removing sharing-aware modeling costs +23% startup / +25% memory; removing layer caching costs +14% / +39%. Parameters α=0.996, p=0.8, n=6 are each the sensitivity-study optimum.Underlined values explain themselves when clicked.

Check your understanding

  1. 1. An idle IR-Py (Python) container’s keep-alive expires, and one minute later a different Python function is invoked. Predict what happens under full-container caching versus RainbowCake.

  2. 2. Lower layers save the least startup time, yet RainbowCake keeps them alive longest. What justifies spending memory on the least valuable layer?

  3. 3. Every TTL is capped at β = α·t̄ / ((1−α)·m̄) regardless of what the arrival model predicts. What invariant does this cap enforce?

  4. 4. FaaSCache achieves ~50 ms better average latency than RainbowCake in the same evaluation. Why does the paper still claim the better trade-off?

Related papers on Datacenter, Cloud & Networking

See all 163 →