Skip to content
About
Main content

An autoscaler that arrives too late

Simulate a modelNeeds Python

Problem

A reactive autoscaler adds capacity after it sees demand, and capacity arrives after a provisioning delay. A burst shorter than that delay is over before the new capacity is ready, so the scaler serves none of it, and the capacity it adds arrives into the quiet afterwards. Only capacity that is already there, headroom, serves a short burst, and headroom is paid for all the time.

Model a reactive autoscaler whose capacity at time t equals the demand it saw 240 seconds earlier. Feed it a demand curve with two 90-second bursts from 100 to 300, and count the demand that goes unserved. Then add permanent headroom (30%, 100%, 200%) and compare the unserved demand and the idle capacity you pay for.

The question. How much of the bursts goes unserved at each headroom, and how much idle capacity does the headroom cost?


Predict

With 30% headroom and a 4-minute provisioning delay, what share of the bursts goes unserved?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    an-autoscaler-that-arrives-too-late.py41 linesDownload
    View the code
    an-autoscaler-that-arrives-too-late.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """An autoscaler that arrives too late.
    
    A reactive autoscaler whose capacity at time t equals the demand it saw DELAY
    seconds earlier (never below a floor of 100 × (1 + headroom)), against a
    demand curve with two 90-second bursts. Unserved and idle demand-seconds for
    headroom from 0% to 200%.
    """
    import argparse, json
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--delay", type=int, default=240, help="Seconds between seeing demand and having the capacity.")
    p.add_argument("--headrooms", default="0,0.3,1.0,2.0", help="Permanent headroom fractions.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    demand = [100] * 100 + [300] * 90 + [100] * 400 + [300] * 90 + [100] * 300   # one value per second
    def evaluate(headroom):
        unserved = idle = 0; cap_series = []
        for t, d in enumerate(demand):
            seen = demand[max(0, t - args.delay)]                              # what the scaler saw DELAY seconds ago
            cap = max(seen, 100 * (1 + headroom))                              # reactive capacity, plus a floor
            unserved += max(0, d - cap); idle += max(0, cap - d); cap_series.append(cap)
        return unserved, idle, cap_series
    rows, timeline = [], []
    burst = sum(max(0, d - 100) for d in demand)
    for h in map(float, args.headrooms.split(",")):
        u, i, caps = evaluate(h)
        rows.append({"headroom_pct": h * 100, "unserved": u, "unserved_pct_of_burst": u / burst * 100, "idle": i})
        if h in (0.0, 2.0): timeline += [{"second": t, "series": f"capacity, headroom {h:.0%}", "value": c} for t, c in enumerate(caps) if t % 5 == 0]
    timeline += [{"second": t, "series": "demand", "value": d} for t, d in enumerate(demand) if t % 5 == 0]
    for r in rows: print(f"headroom={r['headroom_pct']:4.0f}%  unserved={r['unserved']:6.0f} ({r['unserved_pct_of_burst']:5.1f}% of the bursts)  idle={r['idle']:7.0f}   (demand-seconds)")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"headroom": rows, "timeline": timeline}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run an-autoscaler-that-arrives-too-late.py
    uv run an-autoscaler-that-arrives-too-late.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--delay240 Seconds between seeing demand and having the capacity.
--headrooms0, 0.3, 1.0, 2.0 Permanent headroom fractions.

Our run

Demand and capacityApple M2 · 8 cores · 8 GB
capacity, headroom 0%capacity, headroom 200%demand
Data table
Demand and capacity
SecondseriesRequests/s
0capacity, headroom 0%100
5capacity, headroom 0%100
10capacity, headroom 0%100
15capacity, headroom 0%100
20capacity, headroom 0%100
25capacity, headroom 0%100
30capacity, headroom 0%100
35capacity, headroom 0%100
40capacity, headroom 0%100
45capacity, headroom 0%100
50capacity, headroom 0%100
55capacity, headroom 0%100
60capacity, headroom 0%100
65capacity, headroom 0%100
70capacity, headroom 0%100
75capacity, headroom 0%100
80capacity, headroom 0%100
85capacity, headroom 0%100
90capacity, headroom 0%100
95capacity, headroom 0%100
100capacity, headroom 0%100
105capacity, headroom 0%100
110capacity, headroom 0%100
115capacity, headroom 0%100
120capacity, headroom 0%100
125capacity, headroom 0%100
130capacity, headroom 0%100
135capacity, headroom 0%100
140capacity, headroom 0%100
145capacity, headroom 0%100
150capacity, headroom 0%100
155capacity, headroom 0%100
160capacity, headroom 0%100
165capacity, headroom 0%100
170capacity, headroom 0%100
175capacity, headroom 0%100
180capacity, headroom 0%100
185capacity, headroom 0%100
190capacity, headroom 0%100
195capacity, headroom 0%100
200capacity, headroom 0%100
205capacity, headroom 0%100
210capacity, headroom 0%100
215capacity, headroom 0%100
220capacity, headroom 0%100
225capacity, headroom 0%100
230capacity, headroom 0%100
235capacity, headroom 0%100
240capacity, headroom 0%100
245capacity, headroom 0%100
250capacity, headroom 0%100
255capacity, headroom 0%100
260capacity, headroom 0%100
265capacity, headroom 0%100
270capacity, headroom 0%100
275capacity, headroom 0%100
280capacity, headroom 0%100
285capacity, headroom 0%100
290capacity, headroom 0%100
295capacity, headroom 0%100
300capacity, headroom 0%100
305capacity, headroom 0%100
310capacity, headroom 0%100
315capacity, headroom 0%100
320capacity, headroom 0%100
325capacity, headroom 0%100
330capacity, headroom 0%100
335capacity, headroom 0%100
340capacity, headroom 0%300
345capacity, headroom 0%300
350capacity, headroom 0%300
355capacity, headroom 0%300
360capacity, headroom 0%300
365capacity, headroom 0%300
370capacity, headroom 0%300
375capacity, headroom 0%300
380capacity, headroom 0%300
385capacity, headroom 0%300
390capacity, headroom 0%300
395capacity, headroom 0%300
400capacity, headroom 0%300
405capacity, headroom 0%300
410capacity, headroom 0%300
415capacity, headroom 0%300
420capacity, headroom 0%300
425capacity, headroom 0%300
430capacity, headroom 0%100
435capacity, headroom 0%100
440capacity, headroom 0%100
445capacity, headroom 0%100
450capacity, headroom 0%100
455capacity, headroom 0%100
460capacity, headroom 0%100
465capacity, headroom 0%100
470capacity, headroom 0%100
475capacity, headroom 0%100
480capacity, headroom 0%100
485capacity, headroom 0%100
490capacity, headroom 0%100
495capacity, headroom 0%100
500capacity, headroom 0%100
505capacity, headroom 0%100
510capacity, headroom 0%100
515capacity, headroom 0%100
520capacity, headroom 0%100
525capacity, headroom 0%100
530capacity, headroom 0%100
535capacity, headroom 0%100
540capacity, headroom 0%100
545capacity, headroom 0%100
550capacity, headroom 0%100
555capacity, headroom 0%100
560capacity, headroom 0%100
565capacity, headroom 0%100
570capacity, headroom 0%100
575capacity, headroom 0%100
580capacity, headroom 0%100
585capacity, headroom 0%100
590capacity, headroom 0%100
595capacity, headroom 0%100
600capacity, headroom 0%100
605capacity, headroom 0%100
610capacity, headroom 0%100
615capacity, headroom 0%100
620capacity, headroom 0%100
625capacity, headroom 0%100
630capacity, headroom 0%100
635capacity, headroom 0%100
640capacity, headroom 0%100
645capacity, headroom 0%100
650capacity, headroom 0%100
655capacity, headroom 0%100
660capacity, headroom 0%100
665capacity, headroom 0%100
670capacity, headroom 0%100
675capacity, headroom 0%100
680capacity, headroom 0%100
685capacity, headroom 0%100
690capacity, headroom 0%100
695capacity, headroom 0%100
700capacity, headroom 0%100
705capacity, headroom 0%100
710capacity, headroom 0%100
715capacity, headroom 0%100
720capacity, headroom 0%100
725capacity, headroom 0%100
730capacity, headroom 0%100
735capacity, headroom 0%100
740capacity, headroom 0%100
745capacity, headroom 0%100
750capacity, headroom 0%100
755capacity, headroom 0%100
760capacity, headroom 0%100
765capacity, headroom 0%100
770capacity, headroom 0%100
775capacity, headroom 0%100
780capacity, headroom 0%100
785capacity, headroom 0%100
790capacity, headroom 0%100
795capacity, headroom 0%100
800capacity, headroom 0%100
805capacity, headroom 0%100
810capacity, headroom 0%100
815capacity, headroom 0%100
820capacity, headroom 0%100
825capacity, headroom 0%100
830capacity, headroom 0%300
835capacity, headroom 0%300
840capacity, headroom 0%300
845capacity, headroom 0%300
850capacity, headroom 0%300
855capacity, headroom 0%300
860capacity, headroom 0%300
865capacity, headroom 0%300
870capacity, headroom 0%300
875capacity, headroom 0%300
880capacity, headroom 0%300
885capacity, headroom 0%300
890capacity, headroom 0%300
895capacity, headroom 0%300
900capacity, headroom 0%300
905capacity, headroom 0%300
910capacity, headroom 0%300
915capacity, headroom 0%300
920capacity, headroom 0%100
925capacity, headroom 0%100
930capacity, headroom 0%100
935capacity, headroom 0%100
940capacity, headroom 0%100
945capacity, headroom 0%100
950capacity, headroom 0%100
955capacity, headroom 0%100
960capacity, headroom 0%100
965capacity, headroom 0%100
970capacity, headroom 0%100
975capacity, headroom 0%100
0capacity, headroom 200%300
5capacity, headroom 200%300
10capacity, headroom 200%300
15capacity, headroom 200%300
20capacity, headroom 200%300
25capacity, headroom 200%300
30capacity, headroom 200%300
35capacity, headroom 200%300
40capacity, headroom 200%300
45capacity, headroom 200%300
50capacity, headroom 200%300
55capacity, headroom 200%300
60capacity, headroom 200%300
65capacity, headroom 200%300
70capacity, headroom 200%300
75capacity, headroom 200%300
80capacity, headroom 200%300
85capacity, headroom 200%300
90capacity, headroom 200%300
95capacity, headroom 200%300
100capacity, headroom 200%300
105capacity, headroom 200%300
110capacity, headroom 200%300
115capacity, headroom 200%300
120capacity, headroom 200%300
125capacity, headroom 200%300
130capacity, headroom 200%300
135capacity, headroom 200%300
140capacity, headroom 200%300
145capacity, headroom 200%300
150capacity, headroom 200%300
155capacity, headroom 200%300
160capacity, headroom 200%300
165capacity, headroom 200%300
170capacity, headroom 200%300
175capacity, headroom 200%300
180capacity, headroom 200%300
185capacity, headroom 200%300
190capacity, headroom 200%300
195capacity, headroom 200%300
200capacity, headroom 200%300
205capacity, headroom 200%300
210capacity, headroom 200%300
215capacity, headroom 200%300
220capacity, headroom 200%300
225capacity, headroom 200%300
230capacity, headroom 200%300
235capacity, headroom 200%300
240capacity, headroom 200%300
245capacity, headroom 200%300
250capacity, headroom 200%300
255capacity, headroom 200%300
260capacity, headroom 200%300
265capacity, headroom 200%300
270capacity, headroom 200%300
275capacity, headroom 200%300
280capacity, headroom 200%300
285capacity, headroom 200%300
290capacity, headroom 200%300
295capacity, headroom 200%300
300capacity, headroom 200%300
305capacity, headroom 200%300
310capacity, headroom 200%300
315capacity, headroom 200%300
320capacity, headroom 200%300
325capacity, headroom 200%300
330capacity, headroom 200%300
335capacity, headroom 200%300
340capacity, headroom 200%300
345capacity, headroom 200%300
350capacity, headroom 200%300
355capacity, headroom 200%300
360capacity, headroom 200%300
365capacity, headroom 200%300
370capacity, headroom 200%300
375capacity, headroom 200%300
380capacity, headroom 200%300
385capacity, headroom 200%300
390capacity, headroom 200%300
395capacity, headroom 200%300
400capacity, headroom 200%300
405capacity, headroom 200%300
410capacity, headroom 200%300
415capacity, headroom 200%300
420capacity, headroom 200%300
425capacity, headroom 200%300
430capacity, headroom 200%300
435capacity, headroom 200%300
440capacity, headroom 200%300
445capacity, headroom 200%300
450capacity, headroom 200%300
455capacity, headroom 200%300
460capacity, headroom 200%300
465capacity, headroom 200%300
470capacity, headroom 200%300
475capacity, headroom 200%300
480capacity, headroom 200%300
485capacity, headroom 200%300
490capacity, headroom 200%300
495capacity, headroom 200%300
500capacity, headroom 200%300
505capacity, headroom 200%300
510capacity, headroom 200%300
515capacity, headroom 200%300
520capacity, headroom 200%300
525capacity, headroom 200%300
530capacity, headroom 200%300
535capacity, headroom 200%300
540capacity, headroom 200%300
545capacity, headroom 200%300
550capacity, headroom 200%300
555capacity, headroom 200%300
560capacity, headroom 200%300
565capacity, headroom 200%300
570capacity, headroom 200%300
575capacity, headroom 200%300
580capacity, headroom 200%300
585capacity, headroom 200%300
590capacity, headroom 200%300
595capacity, headroom 200%300
600capacity, headroom 200%300
605capacity, headroom 200%300
610capacity, headroom 200%300
615capacity, headroom 200%300
620capacity, headroom 200%300
625capacity, headroom 200%300
630capacity, headroom 200%300
635capacity, headroom 200%300
640capacity, headroom 200%300
645capacity, headroom 200%300
650capacity, headroom 200%300
655capacity, headroom 200%300
660capacity, headroom 200%300
665capacity, headroom 200%300
670capacity, headroom 200%300
675capacity, headroom 200%300
680capacity, headroom 200%300
685capacity, headroom 200%300
690capacity, headroom 200%300
695capacity, headroom 200%300
700capacity, headroom 200%300
705capacity, headroom 200%300
710capacity, headroom 200%300
715capacity, headroom 200%300
720capacity, headroom 200%300
725capacity, headroom 200%300
730capacity, headroom 200%300
735capacity, headroom 200%300
740capacity, headroom 200%300
745capacity, headroom 200%300
750capacity, headroom 200%300
755capacity, headroom 200%300
760capacity, headroom 200%300
765capacity, headroom 200%300
770capacity, headroom 200%300
775capacity, headroom 200%300
780capacity, headroom 200%300
785capacity, headroom 200%300
790capacity, headroom 200%300
795capacity, headroom 200%300
800capacity, headroom 200%300
805capacity, headroom 200%300
810capacity, headroom 200%300
815capacity, headroom 200%300
820capacity, headroom 200%300
825capacity, headroom 200%300
830capacity, headroom 200%300
835capacity, headroom 200%300
840capacity, headroom 200%300
845capacity, headroom 200%300
850capacity, headroom 200%300
855capacity, headroom 200%300
860capacity, headroom 200%300
865capacity, headroom 200%300
870capacity, headroom 200%300
875capacity, headroom 200%300
880capacity, headroom 200%300
885capacity, headroom 200%300
890capacity, headroom 200%300
895capacity, headroom 200%300
900capacity, headroom 200%300
905capacity, headroom 200%300
910capacity, headroom 200%300
915capacity, headroom 200%300
920capacity, headroom 200%300
925capacity, headroom 200%300
930capacity, headroom 200%300
935capacity, headroom 200%300
940capacity, headroom 200%300
945capacity, headroom 200%300
950capacity, headroom 200%300
955capacity, headroom 200%300
960capacity, headroom 200%300
965capacity, headroom 200%300
970capacity, headroom 200%300
975capacity, headroom 200%300
0demand100
5demand100
10demand100
15demand100
20demand100
25demand100
30demand100
35demand100
40demand100
45demand100
50demand100
55demand100
60demand100
65demand100
70demand100
75demand100
80demand100
85demand100
90demand100
95demand100
100demand300
105demand300
110demand300
115demand300
120demand300
125demand300
130demand300
135demand300
140demand300
145demand300
150demand300
155demand300
160demand300
165demand300
170demand300
175demand300
180demand300
185demand300
190demand100
195demand100
200demand100
205demand100
210demand100
215demand100
220demand100
225demand100
230demand100
235demand100
240demand100
245demand100
250demand100
255demand100
260demand100
265demand100
270demand100
275demand100
280demand100
285demand100
290demand100
295demand100
300demand100
305demand100
310demand100
315demand100
320demand100
325demand100
330demand100
335demand100
340demand100
345demand100
350demand100
355demand100
360demand100
365demand100
370demand100
375demand100
380demand100
385demand100
390demand100
395demand100
400demand100
405demand100
410demand100
415demand100
420demand100
425demand100
430demand100
435demand100
440demand100
445demand100
450demand100
455demand100
460demand100
465demand100
470demand100
475demand100
480demand100
485demand100
490demand100
495demand100
500demand100
505demand100
510demand100
515demand100
520demand100
525demand100
530demand100
535demand100
540demand100
545demand100
550demand100
555demand100
560demand100
565demand100
570demand100
575demand100
580demand100
585demand100
590demand300
595demand300
600demand300
605demand300
610demand300
615demand300
620demand300
625demand300
630demand300
635demand300
640demand300
645demand300
650demand300
655demand300
660demand300
665demand300
670demand300
675demand300
680demand100
685demand100
690demand100
695demand100
700demand100
705demand100
710demand100
715demand100
720demand100
725demand100
730demand100
735demand100
740demand100
745demand100
750demand100
755demand100
760demand100
765demand100
770demand100
775demand100
780demand100
785demand100
790demand100
795demand100
800demand100
805demand100
810demand100
815demand100
820demand100
825demand100
830demand100
835demand100
840demand100
845demand100
850demand100
855demand100
860demand100
865demand100
870demand100
875demand100
880demand100
885demand100
890demand100
895demand100
900demand100
905demand100
910demand100
915demand100
920demand100
925demand100
930demand100
935demand100
940demand100
945demand100
950demand100
955demand100
960demand100
965demand100
970demand100
975demand100
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

Unserved and idle demand-seconds

Unserved and idle demand-seconds
HeadroomUnservedOf the burstsIdle
0%36K100%36K
30%30.6K85%54.6K
100%18K50%98K
200%00%160K
Output · uv run an-autoscaler-that-arrives-too-late.py
headroom=   0%  unserved= 36000 (100.0% of the bursts)  idle=  36000   (demand-seconds)
headroom=  30%  unserved= 30600 ( 85.0% of the bursts)  idle=  54600   (demand-seconds)
headroom= 100%  unserved= 18000 ( 50.0% of the bursts)  idle=  98000   (demand-seconds)
headroom= 200%  unserved=     0 (  0.0% of the bursts)  idle= 160000   (demand-seconds)

What we found

With no headroom, 100% of the bursts went unserved, and the capacity added for them arrived afterwards as 36K idle demand-seconds. 30% headroom still left 85% unserved; 100% left 50%; only 200% served the bursts, at 160K idle demand-seconds.


Read it

A scaler cannot react to a burst shorter than its delay; for those, the choice is headroom (pay for idle), prediction (scale before the burst), or shedding (decide who goes unserved). A or load shedding changes how the unserved part feels, not whether it exists.