---
title: Pod
source: 'https://academia.sh/en/courses/orchestration/pod'
course: 'Container Orchestration'
language: en
updated: '2026-09-13T05:08:26+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Names the three things a pod keeps together: the same network identity, the same local filesystem, the same lifetime'
    - 'Compares two packaging decisions by scale unit, failure propagation, resource accounting, and network round trip count'
    - 'Counts the information items a packaging decision depends on and measures the cost of a wrong decision when one item is missing'
---

# Pod

Measuring the smallest deployment unit declared to the cluster: what keeping three things together buys, the scale unit of packaging two processes into one pod versus two pods, and the difference in failure propagation, resource accounting, and network round trips.

The cluster is up, and the reconciliation loop is turning: desired state sits in one place, actual
state is measured in another, and the difference between them is converted into a correction. This
loop was measured — how many rounds it takes to converge, how much work it does per round, how many
wrong corrections it produces with delayed information. But the thing the loop operates on was never
defined. **What is declared to the cluster?** Desired state is not a number; it is an object, and
that object's boundaries determine what the reconciler treats as a single unit and what it corrects
separately.

This topic's first decision starts right there: what is the **smallest piece** that can be declared
to the cluster? The answer is not a single container. The smallest deployment unit is a set of
containers that declares that it will be moved together; this unit is called a pod. This lesson
counts what a pod holds together and compares two packaging decisions across four measurements. What
is measured is a model built with `node`; no real cluster runs.

**CO1 — the regional measurement network runs on a cluster in this course:** this is the fictional
software that collects, verifies, and bills meter readings. **CO2 — the processes inside a pod share
three things:** the same network identity, the same local filesystem, the same lifetime. **CO3 —
every exchange between two processes in separate pods adds one name resolution and one network round
trip**, and inside the same pod this count is zero. **CO4 — the reading collector needs six
replicas, the log transformer two**; the verifier and the local cache need four each, billing and the
work order three each. **CO5 — if a process inside a pod crashes, the whole pod restarts** and goes
two rounds without serving. **CO6 — the resource request and limit are declared at the pod level**;
the processes inside it are not accounted separately.

## Where the Boundary Is Drawn

A log transformer runs alongside the reading collector: it reads the raw lines the collector
produces, formats them, and emits them. Are these two processes packaged in the same pod, or in
separate pods? The question looks like a matter of preference; the model below reduces it to four
numbers.

```js
// pod.mjs — measuring two packaging decisions (MODEL; no real cluster runs).
// CO1-CO6 assumptions are embedded in the code; every number below comes from this file.

export const PROCESSES = {
  'reading-collector':  { replicas: 6, sharedFile: true,  exchanges: 120 },
  'log-transformer':    { replicas: 2, sharedFile: true,  exchanges: 120 },
  'verifier':           { replicas: 4, sharedFile: false, exchanges: 400 },
  'local-cache':        { replicas: 4, sharedFile: false, exchanges: 400 },
  'billing':            { replicas: 3, sharedFile: false, exchanges: 12 },
  'work-order':         { replicas: 3, sharedFile: false, exchanges: 12 },
};

export const TRIPS = 2;                                   // CO3: name resolution + network round trip
export const SHARED = ['network identity', 'local filesystem', 'lifetime'];

// Two packaging decisions keep the same process under the same load; only the boundary changes.
export function pack(a, b) {
  const A = PROCESSES[a], B = PROCESSES[b];
  const max = Math.max(A.replicas, B.replicas), total = A.replicas + B.replicas;
  const exchanges = Math.min(A.exchanges, B.exchanges);
  return {
    exchanges,
    together: { pods: max, procs: 2 * max, extra: 2 * max - total, scaleUnit: 1,
      failures: 2, accounting: max, procAccounting: 0, trips: 0, extraNotify: 0 },
    apart: { pods: total, procs: total, extra: 0, scaleUnit: 2,
      failures: 1, accounting: total, procAccounting: total, trips: TRIPS * exchanges,
      extraNotify: SHARED.length },
  };
}

// Devolved decision: should these two processes travel in the same pod?
export const INFO = ['shared file', 'exchange frequency', 'replica ratio'];
const THRESHOLD = 100;

export function decide(k) {
  const missing = INFO.filter((x) => k[x] === undefined);
  if (missing.length) return { result: 'cannot decide', note: `missing: ${missing.join(', ')}` };
  const tight = k['shared file'] || k['exchange frequency'] >= THRESHOLD;
  if (k['replica ratio'] !== 1 && tight) return { result: 'conflict', note: 'two criteria point opposite ways' };
  if (k['replica ratio'] !== 1) return { result: 'separate pods', note: 'replica ratio is not 1' };
  if (tight) return { result: 'same pod', note: 'tightly coupled, ratio 1' };
  return { result: 'separate pods', note: 'sparse exchange' };
}

export function record(a, b) {
  const A = PROCESSES[a], B = PROCESSES[b];
  return { 'shared file': A.sharedFile && B.sharedFile,
    'exchange frequency': Math.min(A.exchanges, B.exchanges),
    'replica ratio': Math.max(A.replicas, B.replicas) / Math.min(A.replicas, B.replicas) };
}

export const PAIRS = [
  ['reading-collector', 'log-transformer', 'collector+transformer'],
  ['verifier', 'local-cache', 'verifier+cache'],
  ['billing', 'work-order', 'billing+work-order'],
];

if (import.meta.url === `file://${process.argv[1]}`) {
  const s = (x, n) => String(x).padStart(n);
  console.log(`${'pair'.padEnd(23)}${'boundary'.padEnd(12)}${s('pods', 7)}${s('procs', 6)}`
    + `${s('extra', 6)}${s('scale', 6)}${s('fail', 6)}${s('acct', 6)}${s('trips', 6)}`
    + `${s('notify', 9)}`);
  for (const [a, b, ad] of PAIRS) {
    const p = pack(a, b);
    for (const [boundary, r] of [['one pod', p.together], ['two pods', p.apart]]) {
      console.log(`${ad.padEnd(23)}${boundary.padEnd(12)}`
        + `${s(r.pods, 7)}${s(r.procs, 6)}${s(r.extra, 6)}${s(r.scaleUnit, 6)}`
        + `${s(r.failures, 6)}${s(r.procAccounting, 6)}${s(r.trips, 6)}${s(r.extraNotify, 9)}`);
    }
  }

  console.log(`\nshared item (one pod): ${SHARED.join(', ')} = ${SHARED.length}`);
  console.log(`in two pods each one turns into a separate notification item: ${SHARED.length}`);

  console.log(`\n${'pair'.padEnd(28)}${'decision'.padEnd(16)}reason`);
  for (const [a, b, ad] of PAIRS) {
    const r = decide(record(a, b));
    console.log(`${ad.padEnd(28)}${r.result.padEnd(16)}${r.note}`);
  }
  const missingK = { ...record('billing', 'work-order') };
  delete missingK['replica ratio'];
  const r = decide(missingK);
  console.log(`${'billing+work-order (blind)'.padEnd(28)}${r.result.padEnd(16)}${r.note}`);

  // The cost of information: if 'replica ratio' is unknown, the system still has to pack.
  let wrong = 0, extraProcs = 0;
  for (const [a, b] of PAIRS) {
    const k = record(a, b);
    const full = decide(k);
    const blindK = { ...k }; delete blindK['replica ratio'];
    const blind = blindK['shared file'] || blindK['exchange frequency'] >= THRESHOLD
      ? 'same pod' : 'separate pods';
    const correct = full.result === 'same pod' ? 'same pod' : 'separate pods';
    if (blind !== correct) { wrong++; extraProcs += pack(a, b).together.extra; }
  }
  console.log(`\nif replica ratio is unknown: ${wrong} of ${PAIRS.length} pairs mispacked,`
    + ` ${extraProcs} unnecessary process instances`);
}
```

```
pair                   boundary       pods procs extra scale  fail  acct trips   notify
collector+transformer  one pod           6    12     4     1     2     0     0        0
collector+transformer  two pods          8     8     0     2     1     8   240        3
verifier+cache         one pod           4     8     0     1     2     0     0        0
verifier+cache         two pods          8     8     0     2     1     8   800        3
billing+work-order     one pod           3     6     0     1     2     0     0        0
billing+work-order     two pods          6     6     0     2     1     6    24        3

shared item (one pod): network identity, local filesystem, lifetime = 3
in two pods each one turns into a separate notification item: 3

pair                        decision        reason
collector+transformer       conflict        two criteria point opposite ways
verifier+cache              same pod        tightly coupled, ratio 1
billing+work-order          separate pods   sparse exchange
billing+work-order (blind)  cannot decide   missing: replica ratio

if replica ratio is unknown: 1 of 3 pairs mispacked, 4 unnecessary process instances
```

The first pair shows why the pod boundary is not a matter of preference. The collector wants six
replicas, the transformer two. In one pod the scale unit is **one**: six pods open, twelve process
instances run, and four of them serve no load at all. Four extra process instances are the direct
cost of the scale unit being one — the transformer has ridden along on the collector's scale even
though it did not need to. In two pods these four instances disappear, and in exchange two hundred
forty network round trips appear: each of the hundred twenty exchanges pays one name resolution and
one network round trip.

The second pair gives the reverse of the same accounting. The verifier and the local cache have
equal replica counts, so in one pod the extra process instances are **zero**; separating them buys
nothing, and eight hundred network round trips are paid instead. Tightly coupled helper processes
that scale at the same rate belong inside the pod boundary; this is the reason the boundary exists.
The third pair shows the third case: equal scale, but sparse exchange. Twenty-four round trips are
cheap; separating is reason enough to bring failure propagation down from two to one and to gain
per-process resource accounting.

The resource accounting column is a direct consequence of **CO6**. In one pod there are six
accounting entries but zero per-process accounting: the pod's request and limit are the sum of the
two processes, and which one consumes how much never shows up in the declaration. In two pods there
are eight entries and eight per-process accounts. This difference feeds into the scaling decision —
a system that does not know what is growing scales both of them at once.

The last row counts the notification cost. In one pod the three shared items are free because the
boundary itself provides them. Separating turns all three into a notification item: the network
identity into a service object, the local filesystem into a transfer format, and the shared lifetime
into a guarantee that no longer exists. The compose file on a single machine also gathers several
processes into one file; the difference is that togetherness there means *being on the same
machine*, while in a pod it means *moving together, dying together, and sharing a single identity*.
The compose file's startup waves and readiness-check measurement were done there; they are not
repeated here, they become an input to the comparison.

## The Cost of the Same Lifetime

Scale, round-trip, and accounting counts can all be read from a fixed table. The third shared item —
the same lifetime — only becomes visible over time. The second run applies the same failure sequence
to both packagings.

```js
// lifetime.mjs — the cost of the same lifetime: the same failure sequence applied to two packagings (MODEL).
import { PROCESSES } from './pod.mjs';

const SEED = 20260801, ROUNDS = 24, REPAIR = 2, RATE = 0.35;
// The generator is hand-written; 32-bit integer operations, seed is visible.
const make = (t) => () => { t ^= t << 13; t ^= t >>> 17; t ^= t << 5; return (t >>> 0) / 2 ** 32; };

// Required process instances: 6 collectors + 2 transformers. The failure sequence is generated once.
const NEEDED = [
  ...Array.from({ length: PROCESSES['reading-collector'].replicas }, (_, i) => ['collector', i]),
  ...Array.from({ length: PROCESSES['log-transformer'].replicas }, (_, i) => ['transformer', i]),
];
const rnd = make(SEED);
const FAILURES = Array.from({ length: ROUNDS },
  () => (rnd() < RATE ? NEEDED[Math.floor(rnd() * NEEDED.length)] : null));

// One pod: pod i carries both collector i and transformer i; if one falls, both fall.
// Two pods: each process instance is in its own pod; a failure only takes it down.
function run(together) {
  const down = {};                                       // process instance -> down until when
  let fell = 0, neighbor = 0;
  const missingRounds = { collector: 0, transformer: 0 };
  for (let t = 0; t < ROUNDS; t++) {
    if (FAILURES[t]) {
      const [role, i] = FAILURES[t];
      // Pod i, at the one-pod boundary, carries both collector i and transformer i. CO5.
      for (const [r, k] of together ? [['collector', i], ['transformer', i]] : [[role, i]]) {
        const key = `${r}#${k}`;
        if (!NEEDED.some(([x, y]) => x === r && y === k)) continue;
        if ((down[key] ?? -1) < t) { fell++; if (r !== role) neighbor++; }
        down[key] = t + REPAIR;
      }
    }
    for (const role2 of ['collector', 'transformer']) {
      const ad = role2 === 'collector' ? 'reading-collector' : 'log-transformer';
      const up = Array.from({ length: PROCESSES[ad].replicas }, (_, k) => `${role2}#${k}`)
        .filter((a) => (down[a] ?? -1) < t).length;
      if (up < PROCESSES[ad].replicas) missingRounds[role2]++;
    }
  }
  return { fell, neighbor, ...missingRounds };
}

const s = (x, n) => String(x).padStart(n);
console.log(`seed ${SEED}, ${ROUNDS} rounds, ${FAILURES.filter(Boolean).length} failures,`
  + ` repair ${REPAIR} rounds`);
console.log(`${'boundary'.padEnd(12)}${s('fell', 7)}${s('from neighbor', 16)}`
  + `${s('collector short', 17)}${s('transformer short', 19)}`);
for (const [ad, b] of [['one pod', true], ['two pods', false]]) {
  const r = run(b);
  console.log(`${ad.padEnd(12)}${s(r.fell, 7)}${s(r.neighbor, 16)}${s(`${r.collector} rounds`, 17)}`
    + `${s(`${r.transformer} rounds`, 19)}`);
}
```

```
seed 20260801, 24 rounds, 11 failures, repair 2 rounds
boundary       fell   from neighbor  collector short  transformer short
one pod          13               3        22 rounds           7 rounds
two pods         10               0        20 rounds           4 rounds
```

Eleven failures produce thirteen and ten drops across the two packagings. The gap of three and the
number three in the third column say the same thing: **in one pod, three process instances fell
because of a failure that had nothing to do with them.** In none of these three drops was there a
problem in the transformer's or the collector's own code; its neighbor crashed, the pod restarted,
and it went down with it. At the two-pod boundary this column is zero — the failure never crosses the
boundary drawn by the declaration.

The last column is how this shows up in service. The transformer ran with a missing replica in seven
of twenty-four rounds in one pod, and in four when separated. A gap of three rounds looks small; what
actually grows is not the ratio but the **cause**: nearly half of the transformer's missing rounds
come not from its own failure but from its neighbor's, and this share grows with the number of
processes placed in the pod. The same lifetime is declared as a convenience and paid for as a source
of outages.

## The Devolved Decision and What It Does Not Know

The decision that moves from human to system in this lesson is: **which processes travel as a single
unit.** Once the decision is made, placement, restart, and scaling all operate on this unit; the
system never looks inside the pod. The information items the decision depends on are named in the
model, and there are three of them: shared file use, exchange frequency, replica ratio. All three
have a source — the first two come from measurement, the third from a load declaration.

The cost of information is most visible in the third item. The replica ratio is not a measurement but
a **guess**: will the two processes' future load grow in the same direction? If this guess is not
written down, the system still has to pack, and the model's last line states what happens: one of
three pairs is mispacked, four unnecessary process instances appear. The source of the error is not a
bug but a **gap**; a rule that does not know the replica ratio takes the option that looks cheap and
puts the collector and the transformer in the same pod.

The first pair's decision, though, is not resolved even with information: because the two criteria
point in opposite directions, the result comes out as `conflict`. The shared file and frequent
exchange argue for togetherness, while the threefold replica ratio argues for separation. This is a
case where devolving **does not pay off**: the system applies the rule, the rule produces no answer,
and the decision returns to a human. An answer only emerges once one of the inputs changes — either
the four extra process instances are accepted, or the shared file is converted into a transfer and
two hundred forty round trips are paid. Devolving the decision forces a change in one of the
quantities the decision depends on.

## Summary

- A pod is the smallest deployment unit that can be declared to the cluster, and it keeps three
  things together: the same network identity, the same local filesystem, the same lifetime.
  Separating turns these three into three separate notification items.
- If two processes asking for six and two replicas are packed into one pod, the scale unit drops to
  one and four unnecessary process instances appear; if they are separated, these four instances
  disappear and two hundred forty network round trips are added.
- For the pair with an equal replica ratio, separating gains nothing and costs eight hundred round
  trips; for the pair that scales equally but exchanges rarely, failure propagation drops from two to
  one in exchange for twenty-four round trips.
- Resource accounting is at the pod level: one pod keeps six entries, per-process accounting is zero,
  and what is growing never shows up in the declaration.
- On the same failure sequence, one pod produced thirteen drops and two pods produced ten; the entire
  difference is neighbor failure, and the transformer ran with a missing replica for seven rounds
  instead of four.
- The devolved decision rests on three information items; when the replica ratio is missing, one of
  three pairs is mispacked and four unnecessary process instances appear, and when the criteria
  conflict the rule produces no answer.

## Next Step

This lesson drew the boundary of a single pod and took the number of replicas it needs as a given:
six, four, three. Yet that number is itself something declared to the cluster, and the moment it is
declared a new question arises — when the actual replica count drifts from what is desired, who
corrects it, how fast, and in what order? Moreover, while the desired replica count stays fixed,
**what the replica contains** can change; moving to a new version requires declaring how many extra
and how many missing replicas are allowed at the same time. The next lesson writes both objects with
`node` and counts the rollout round by round: how many rounds it takes to complete, how many rounds
it runs below capacity, and how many rounds a rollback takes.
