---
title: 'Versioning and Aliases'
source: 'https://academia.sh/en/courses/serverless/versioning-and-aliases'
course: 'Serverless and Edge Computing'
language: en
updated: '2026-09-13T05:07:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Reads two numbers at every step of the weight ladder: the requests that fall to the new version and the first round where the decision can be made'
    - 'Shows cold start interfering with the canary measurement: as the share shrinks, the duration a single cold call adds to the average grows'
    - 'Counts rollback being a single write, and how much that changes once the old pool empties after a full cutover'
---

# Versioning and Aliases

Weight shifting through the alias is measured: the version stays unchanged, the alias moves. At every step, the affected requests and the round where the difference disappears are counted; cold start's interference with the canary measurement and rollback being a single key turn are worked out separately.

Thresholds were counted: the duration limit, the memory limit, the concurrency cap. Which job does
not fit inside these thresholds was written down. What happens to the job that does fit was never
asked — once that function is written, how does it enter the running system?

The pattern side of gradual release — by which metric, which window, which threshold the decision
is made — is a topic measured elsewhere and is not repeated here. The question here is narrower:
this ladder runs through a single pointer called an **alias**, and the mechanism has two facts of
its own. Rollback is a single write; cold start starts from zero on the new version.

- **FC25.** The fictional regional measurement network's validation function receives nine hundred
  requests a minute; variation by time of day is not modeled. Both the network and the numbers are
  fictional.
- **FC26.** On a warm instance processing takes two hundred twenty milliseconds; cold start adds
  one thousand eight hundred milliseconds. Package size's effect is outside this lesson.
- **FC27.** An idle instance is dropped four minutes later. **The pool is separate per version**:
  even though the alias moves, the two versions' pools do not merge.
- **FC28.** The gate reads a single figure: the new version's average latency. The baseline is
  measured while weight is zero, and the threshold is one point two five times the baseline.
- **FC29.** Requests arrive at exponential intervals; the generator is self-written, and the seed
  is visible. Every weight step contains five two-minute rounds.
- **FC30.** The alias write, propagation, and republish durations are fictional.

## The Version Stays Fixed, the Alias Moves

What is measured is not a real runtime but a model built with `node`. There are two objects: a
**version**, a frozen record whose digest never changes; an **alias**, a pointer that points at
one of these versions and can be overwritten. A separate warm pool is kept per version.

```js
// alias.mjs — version, alias, and warm pool model; not a real runtime, a model
export const SEED = 20260420, REQ_MIN = 900;   // fictional: requests per minute to the validation function
export const SERVICE_MS = 220, COLD_EXTRA_MS = 1800, IDLE_MS = 240000;  // warm, cold extra, idle lifetime
export const ROUND_MIN = 2, ROUNDS = 5, GATE = 1.25, REGRESSION_MS = 120;
export const WEIGHTS = [1, 2, 5, 25, 100];

export function rng(seed) {                      // linear congruential generator, seed is visible
  let x = seed >>> 0;
  return () => { x = (Math.imul(x, 1664525) + 1013904223) >>> 0; return x / 4294967296; };
}

// A version is frozen: its digest never changes again. An alias is a pointer that can be overwritten.
export const version = (label, digest) => Object.freeze({ label, digest });
export const alias = (name, target) => ({ name, target, writes: 0 });
export const move = (a, target) => Object.assign(a, { target, writes: a.writes + 1 });

// The pool is per version: an idle instance runs warm if found, otherwise a new instance is opened.
export const pool = () => ({ instances: [], cold: 0 });
export const alive = (p, t) => p.instances.filter((o) => o.last + IDLE_MS > t).length;
export function call(p, t, extraMs) {
  p.instances = p.instances.filter((o) => o.last + IDLE_MS > t);
  const o = p.instances.find((x) => x.busy <= t), duration = SERVICE_MS + extraMs + (o ? 0 : COLD_EXTRA_MS);
  if (o) { o.busy = t + duration; o.last = t + duration; }
  else { p.cold += 1; p.instances.push({ busy: t + duration, last: t + duration }); }
  return duration;
}

// A step: requests arrive at exponential intervals, and are routed to the old or new version by weight.
export function step(rnd, poolOld, poolNew, weight, extraMs, t0, rounds = ROUNDS, roundMin = ROUND_MIN) {
  const lam = REQ_MIN / 60000, roundData = [];
  let t = t0 - Math.log(1 - rnd()) / lam;
  for (let k = 0; k < rounds; k += 1) {
    const end = t0 + (k + 1) * roundMin * 60000;
    let nNew = 0, nOld = 0, sumNew = 0, sumOld = 0, coldStart = poolNew.cold;
    while (t < end) {
      if (rnd() * 100 < weight) { sumNew += call(poolNew, t, extraMs); nNew += 1; } else { sumOld += call(poolOld, t, 0); nOld += 1; }
      t -= Math.log(1 - rnd()) / lam;
    }
    roundData.push({ nNew, avgNew: nNew ? sumNew / nNew : 0, avgOld: nOld ? sumOld / nOld : 0, cold: poolNew.cold - coldStart });
  }
  return roundData;
}

// Ladder: baseline is read first while weight is zero, then weight grows. Pools carry over between
// steps: the new version's pool is empty only on the first step.
export function ladder(seed, extraMs, weights = WEIGHTS, warm = 0) {
  const rnd = rng(seed), poolOld = pool(), poolNew = pool();
  const baseline = step(rnd, poolOld, poolNew, 0, 0, 0, ROUNDS).reduce((x, y) => x + y.avgOld, 0) / ROUNDS;
  const rows = [];
  let t = ROUNDS * ROUND_MIN * 60000, affected = 0;
  for (let i = 0; i < warm; i += 1) call(poolNew, t - 30000, 0);   // synchronous synthetic warm-up calls
  for (const w of weights) {
    const roundData = step(rnd, poolOld, poolNew, w, extraMs, t, ROUNDS);
    t += ROUNDS * ROUND_MIN * 60000;
    affected += roundData.reduce((x, y) => x + y.nNew, 0);
    rows.push({ weight: w, roundData, affected });
  }
  return { baseline, rows };
}

export const sign = (x) => `${x < 0 ? '' : '+'}${Math.round(x)}`;
export const row = (g, ...x) => x.map((v, i) => String(v).padEnd(g[i])).join('');
```

The handed-off decision shows up here: which request goes to which version, how many instances
open, is not written down — a single number is written in its place. The constraint sits in the
same spot: because the pool is separate per version, the new version does its warming from zero.

## Two Numbers at Every Step

```js
// run1.mjs — alias weight is shifted step by step; cold calls, latency, and the gate are read at each step
import { ladder, version, alias, move, sign, row, SEED, REQ_MIN, SERVICE_MS,
  COLD_EXTRA_MS, GATE, REGRESSION_MS, WEIGHTS } from './alias.mjs';

const v7 = version('v7', 'e3bb1237'), v8 = version('v8', 'd8bb00e6'), live = alias('live', v7);
try { v7.digest = 'overwritten'; } catch { /* writing to a frozen record is rejected */ }
console.log(`seed ${SEED} | ${REQ_MIN} req/min | warm ${SERVICE_MS} ms | cold extra ${COLD_EXTRA_MS} ms`);
console.log(`alias "${live.name}" -> ${live.target.label}, moved -> ${move(live, v8).target.label}` +
  ` | writes ${live.writes} | digest ${v7.digest} (could not be overwritten)`);

const G = [9, 11, 13, 12, 12, 13, 13, 13];
for (const [label, extra, show] of [['clean version', 0, WEIGHTS],
  [`flawed version (+${REGRESSION_MS} ms real regression)`, REGRESSION_MS, [1]]]) {
  const { baseline, rows } = ladder(SEED, extra);
  console.log(`\n${label} | ladder ${WEIGHTS.join('-')} | baseline ${Math.round(baseline)} ms | gate threshold ${Math.round(baseline * GATE)} ms`);
  console.log(row(G, 'weight', 'req/round', 'round1 cold', 'round1 avg', 'round2 avg',
    'round1 diff', 'round2 diff', 'first clean') + 'affected');
  for (const s of rows.filter((x) => show.includes(x.weight))) {
    const [a, b] = s.roundData, t = s.roundData.findIndex((x) => x.avgNew <= baseline * GATE);
    console.log(row(G, `${s.weight}%`, a.nNew, a.cold, Math.round(a.avgNew), Math.round(b.avgNew),
      sign(a.avgNew - baseline), sign(b.avgNew - baseline), t < 0 ? 'none' : `round ${t + 1}`) + s.affected);
  }
}
```

```
seed 20260420 | 900 req/min | warm 220 ms | cold extra 1800 ms
alias "live" -> v7, moved -> v8 | writes 1 | digest e3bb1237 (could not be overwritten)

clean version | ladder 1-2-5-25-100 | baseline 226 ms | gate threshold 283 ms
weight   req/round  round1 cold  round1 avg  round2 avg  round1 diff  round2 diff  first clean  affected
1%       21         2            391         220         +165         -6           round 2      85
2%       38         1            267         220         +41          -6           round 1      266
5%       99         1            238         220         +12          -6           round 1      717
25%      468        2            228         220         +1           -6           round 1      2968
100%     1823       7            227         220         +1           -6           round 1      11969

flawed version (+120 ms real regression) | ladder 1-2-5-25-100 | baseline 226 ms | gate threshold 283 ms
weight   req/round  round1 cold  round1 avg  round2 avg  round1 diff  round2 diff  first clean  affected
1%       21         2            511         340         +285         +114         none         85
```
The second line confirms the lesson's title: the alias moved from `v7` to `v8` — one write — and
at the same moment, the attempt to write to `v7`'s digest was rejected. Release is not changing
records; it is updating a single field that can be overwritten.

The affected requests column writes the gain from shifting: eighty-five at the end of the 1% step,
eleven thousand nine hundred sixty-nine once it reaches 100% — a difference close to one hundred
forty times.

The round-where-it-clears column, though, breaks down in an unexpected place. In the clean
version, the **first round of the 1% step trips the gate**: the average is three hundred ninety-one
milliseconds, the threshold two hundred eighty-three. The same version drops to two hundred twenty
in the second round; in the flawed version the difference stays around one hundred fourteen
milliseconds in every round. What distinguishes the two is not the size of the difference but
**whether it dies down**.

The reason is that the pool is separate per version: the new version enters the first step with an
empty pool, opens two instances, and those instances stay warm for the rest of the ladder. The
cold start cost falls only on the first step — by definition the step with the smallest sample.
The arithmetic is one line: the gate's margin is fifty-seven milliseconds, the 1% share carries
eighteen requests per round, and a single cold call of one thousand eight hundred milliseconds
adds one hundred milliseconds to the average; fifty at 2%, twenty at 5%. A small share both lowers
the affected requests and makes the measurement sensitive to cold start — the first was wanted, the
second arrived as a constraint.

## Rollback Is a Single Key Turn

```js
// run2.mjs — two rollback paths, the old pool after a full cutover, the cost of the workaround paths
import { ladder, rng, pool, step, alive, sign, row, SEED, REQ_MIN,
  IDLE_MS, ROUND_MIN, GATE, WEIGHTS } from './alias.mjs';

// Fictional durations: write 120, propagation 900, package load (42 MB / 20 MB-s) 2100, version 3000 ms.
const PATHS = { 'alias flip': [120, 900], redeploy: [2100, 3000, 120, 900] };
console.log(row([20, 7, 11], 'rollback path', 'steps', 'duration') + 'requests hitting the flawed version in this time');
for (const [name, a] of Object.entries(PATHS)) {
  const ms = a.reduce((x, y) => x + y, 0);
  console.log(row([20, 7, 11], name, a.length, `${(ms / 1000).toFixed(2)} s`) + Math.round(REQ_MIN / 60000 * ms));
}

// After a full cutover the old version's pool empties; rolling back has to reopen that pool.
function cutoverAndRollback(waitMin) {
  const rnd = rng(SEED), poolOld = pool(), poolNew = pool();
  step(rnd, poolOld, poolNew, 0, 0, 0, 5, 1);                          // old version warms up
  const moved = step(rnd, poolOld, poolNew, 100, 0, 300000, waitMin, 1).reduce((x, y) => x + y.nNew, 0);
  const t = 300000 + waitMin * 60000, remaining = alive(poolOld, t), coldStart = poolOld.cold;
  const rollback = step(rnd, poolOld, poolNew, 0, 0, t, 1, 1)[0];      // alias rolled back: one minute is read
  return { moved, remaining, cold: poolOld.cold - coldStart, avg: Math.round(rollback.avgOld) };
}
const H = [9, 25, 29, 19];
console.log(`
rollback after a full cutover (100%) — an idle instance is dropped after ${IDLE_MS / 60000} min`);
console.log(row(H, 'wait', 'moved to new version', 'instances left in old pool', 'cold on rollback') + 'first-min avg');
for (const b of [3, 4, 5]) {
  const g = cutoverAndRollback(b);
  console.log(row(H, `${b} min`, g.moved, g.remaining, g.cold) + `${g.avg} ms`);
}

// Ways to escape cold start on the first step; each path's cost is a separate line item.
const margin = ladder(SEED, 0).baseline * (GATE - 1), Y = [32, 16, 16, 13, 11, 12, 13];
console.log(`
ways to escape cold start on the first step — gate margin ${Math.round(margin)} ms`);
console.log(row(Y, 'path', 'ladder', 'decision round', 'publish min', 'synthetic', 'in round1', 'round1 cold') + 'diff at decision round');
for (const [label, weights, warm, decisionRound] of [['skip round1, decide on round2', WEIGHTS, 0, 2],
  ['1 synchronous synthetic call', WEIGHTS, 1, 1], ['3 synchronous synthetic calls', WEIGHTS, 3, 1],
  ['make the first step 25%', [25, 100], 0, 1]]) {
  const { baseline, rows } = ladder(SEED, 0, weights, warm), round1 = rows[0].roundData[0];
  console.log(row(Y, label, weights.join('-'), decisionRound, weights.length * decisionRound * ROUND_MIN, warm, round1.nNew, round1.cold) +
    `${sign(rows[0].roundData[decisionRound - 1].avgNew - baseline)} ms`);
}
```

```
rollback path       steps  duration   requests hitting the flawed version in this time
alias flip          2      1.02 s     15
redeploy            4      6.12 s     92

rollback after a full cutover (100%) — an idle instance is dropped after 4 min
wait     moved to new version     instances left in old pool   cold on rollback   first-min avg
3 min    2680                     12                           0                  220 ms
4 min    3584                     4                            14                 247 ms
5 min    4490                     0                            39                 296 ms

ways to escape cold start on the first step — gate margin 57 ms
path                            ladder          decision round  publish min  synthetic  in round1   round1 cold  diff at decision round
skip round1, decide on round2   1-2-5-25-100    2               20           0          21          2            -6 ms
1 synchronous synthetic call    1-2-5-25-100    1               10           1          21          1            +79 ms
3 synchronous synthetic calls   1-2-5-25-100    1               10           3          21          0            -6 ms
make the first step 25%         25-100          1               4            0          477         5            +13 ms
```
The first table shows what the version being unchangeable buys. Because the old version stands as
a record, rollback is two steps — one write and propagation — one point zero two seconds in total,
and fifteen requests hitting the flawed version. If the version were not kept, the path would climb
to four steps, six point one two seconds, and ninety-two requests; a sixfold difference.

The second table writes the limit of this comfort. Once weight is raised to 100%, the old version
receives no requests at all, and its pool empties in four minutes. Rolled back three minutes later,
twelve instances were still standing in the pool, and no cold call occurred; five minutes later
zero instances remain, and the first minute pays thirty-nine cold calls, pushing the average up to
two hundred ninety-six. Rollback is still a single write, but its cost comes not from the operation
but from the time that has passed.

The third table writes the bill for the workaround. Discarding the first round clears the decision,
but stretches the publish from ten minutes to twenty. Pre-warming keeps the duration, but a single
synthetic call is not enough: the pile-up inside the round opens a second instance, and the
difference stays at seventy-nine milliseconds. Three synchronous calls zero out the cold count;
their cost is fifteen synthetic calls per ladder — requests that mix into the metric and never come
from a real user. Starting the ladder from the top is the most expensive: the difference drops to
thirteen milliseconds, but the affected requests in the first round climb from twenty-one to four
hundred seventy-seven.

## Summary

- The alias moved from `v7` to `v8`: a single write. At the same moment, the attempt to write to
  the version's digest was rejected — release is not changing records, it is updating a pointer.
- Affected requests are eighty-five at the end of the 1% step, eleven thousand nine hundred
  sixty-nine once it reaches 100%.
- A clean version tripped the gate in the first round of the 1% step: two cold calls, three hundred
  ninety-one milliseconds, threshold two hundred eighty-three. It dropped to two hundred twenty in
  the second round; in the flawed version the difference never died down in any round.
- At the 1% share, eighteen requests fall to a round and a single cold call adds one hundred
  milliseconds to the average; the gate's margin is fifty-seven.
- Rollback is two steps and one point zero two seconds; if the version were not kept, it would be
  four steps, six point one two seconds, and ninety-two requests.
- Five minutes after a full cutover, zero instances remain in the old pool; rollback's first minute
  pays thirty-nine cold calls and the average climbs to two hundred ninety-six.

## Next Step

The same number stood still like a constant everywhere in the measurement: the eighteen hundred
milliseconds cold start adds. It was what broke the canary round, wrote rollback's delayed cost,
and made pre-warming necessary. But that number is not constant — the bigger the package, the more
bytes there are to download. The next lesson measures the package: it scans the relationship
between size and cold start, counts what moving dependencies into a shared layer shrinks and what
it does not, works out how many functions version pinning and a layer update touch — and writes the
cost of an unused dependency as bytes moved.
