---
title: 'Push and Pull Based Distribution'
source: 'https://academia.sh/en/courses/traffic-layer/push-and-pull-based-distribution'
course: 'The Traffic Layer'
language: en
updated: '2026-08-23T07:01:35+00:00'
license: 'CC BY-SA 4.0'
---

# Push and Pull Based Distribution

Propagating content to edges with two distinct models: pull-based distribution producing the first miss and a stale-version window, push-based distribution producing wasted copies and a message multiplier, choosing between the two by content class, and the choice's effect on the introductory course's write bandwidth and storage calculations.

The edge cache in the previous lesson waited for the request: the first time a key was asked for,
it missed, went to the origin, and stored the response. The measurement also counted those first
requests — 20,046 of the 200,000 requests missed at the edge, each a trip to the origin. This
waiting is not mandatory.

Content can be sent to the edge before it is ever asked for. There are two ways, and the choice
between them is a propagation model decision. **Pull-based distribution** pulls content on the
first request: the edge misses, goes to the origin, stores the response. **Push-based
distribution** sends content to edges before any request arrives: the edge never misses, but
whether the pushed content will ever be requested is unknown. This lesson measures both models on
the same content set.

## What Marks the Difference

The difference between the two models shows up in three places. First, **origin requests**: in
pull, every edge goes to the origin at least once per object; in push, never. Second, **wasted
copies**: push sends an object even to edges that will never request it. Third, **stale
versions**: when an object changes, pull's edges keep serving the old version until their TTL
expires, while push sends the new version to every edge in one step.

All three tie back to a single quantity: how many edges request an object. If every edge requests
it, push wastes nothing; if only one edge does, all but one of push's copies are wasted. This
lesson measures that.

## Setup

The model is in-process, and a round is an abstract step, the same kind of abstraction as the
introductory course's round. Content splits into two classes. **Common** objects are the tracking
page's shell, style, and script files: requested from every edge. **Unique** objects are
per-shipment tracking responses, and their count comes from the previous lesson's measurement —
20,046 distinct responses in the window.

| Code | Assumption | Value | Rationale |
|---|---|---|---|
| T10 | edge count | 12 | four entry points in each of the three zones |
| T11 | common object count | 20 | shell, style, script, and icon files |
| T12 | edges requesting a unique object | 1 | a shipment's queries come from the same user, the same edge |

```js
// propagation/model.mjs — in-process model: the same content set is distributed first pull
// based, then push based. A round is an abstract step; TTL and the update round are parameters.
export const EDGES = 12;        // T10: edge count
export const TTL = 300;         // T1: TTL (rounds)
export const ROUNDS = 900, UPDATE = 100;

// T11: common objects are requested from every edge. T12: a shipment's queries come from a
// single edge, so a unique object is requested from only one edge. UNIQUE is the previous lesson's measurement.
export const CLASS = [
  { name: "common", objects: 20, requesting: EDGES },
  { name: "unique", objects: 20_046, requesting: 1 },
];

export function cost(cls) {
  const pull = cls.objects * cls.requesting;        // one trip to the origin on the first miss
  const push = cls.objects * EDGES;                 // the set is pushed to every edge
  return { pull, push, wasted: push - pull, wastedShare: 1 - cls.requesting / EDGES };
}

// mode: "pull", "push", or "hybrid" (common objects pushed, unique objects pulled)
const modeOf = (mode, name) => (mode === "hybrid" ? (name === "common" ? "push" : "pull") : mode);

export function propagate(mode) {
  const s = { origin: 0, pushed: 0, wasted: 0, stale: 0, messages: 0 };
  for (const cls of CLASS) {
    const m = cost(cls);
    if (modeOf(mode, cls.name) === "push") { s.pushed += m.push; s.wasted += m.wasted; s.messages += m.push; }
    else { s.origin += m.pull; s.messages += m.pull; }
  }
  const commonMode = modeOf(mode, "common");
  const version = new Array(EDGES).fill(1);          // the common object's version each edge sees
  const expires = Array.from({ length: EDGES }, (_, k) => 1 + ((k * 27) % TTL));
  for (let round = 1; round <= ROUNDS; round++) {
    if (commonMode === "push" && round === UPDATE) { version.fill(2); s.messages += EDGES; }
    for (let k = 0; k < EDGES; k++) {
      if (commonMode === "pull" && round >= expires[k]) {  // TTL expired, refetched
        version[k] = round >= UPDATE ? 2 : 1;
        expires[k] = round + TTL;
        s.origin += 1; s.messages += 1;
      }
      if (round >= UPDATE && version[k] === 1) s.stale += 1;   // round in which the stale version was served
    }
  }
  return s;
}
```

```js
// propagation/measure.mjs — comparing two propagation modes on the same content set
import { propagate, cost, CLASS, EDGES, TTL, ROUNDS, UPDATE } from "./model.mjs";

console.log(`${EDGES} edges, TTL ${TTL} rounds, ${ROUNDS} rounds, ` +
  `common object's version changes at round ${UPDATE}`);
console.log();
console.log("class  | objects | requesting edges | pull req | push req | wasted | wasted share");
console.log("-------|---------|-------------------|----------|----------|--------|-------------");
for (const cls of CLASS) {
  const m = cost(cls);
  console.log(`${cls.name.padEnd(6)} | ${String(cls.objects).padStart(7)} | ` +
    `${String(cls.requesting).padStart(17)} | ${String(m.pull).padStart(8)} | ` +
    `${String(m.push).padStart(8)} | ${String(m.wasted).padStart(6)} | ` +
    `${`%${(m.wastedShare * 100).toFixed(1)}`.padStart(11)}`);
}
console.log();
console.log("mode   | origin req | pushed obj | wasted | stale version rounds | messages");
console.log("-------|------------|------------|--------|-----------------------|---------");
const record = {};
for (const mode of ["pull", "push", "hybrid"]) {
  const r = record[mode] = propagate(mode);
  console.log(`${mode.padEnd(6)} | ${String(r.origin).padStart(10)} | ` +
    `${String(r.pushed).padStart(10)} | ${String(r.wasted).padStart(6)} | ` +
    `${String(r.stale).padStart(21)} | ${String(r.messages).padStart(8)}`);
}
const pull = record.pull, push = record.push;
console.log();
console.log(`message ratio push/pull = ${(push.messages / pull.messages).toFixed(2)}`);
console.log(`stale version rounds, average per edge = ${(pull.stale / EDGES).toFixed(1)} rounds ` +
  `(${((pull.stale / EDGES) / TTL).toFixed(2)} times the TTL)`);
const two = cost({ ...CLASS[1], requesting: 2 });
console.log(`T12 sensitivity: if the unique object were requested from two edges, wasted share ` +
  `%${(two.wastedShare * 100).toFixed(1)} (%${(cost(CLASS[1]).wastedShare * 100).toFixed(1)} at one edge)`);
```

```
12 edges, TTL 300 rounds, 900 rounds, common object's version changes at round 100

class  | objects | requesting edges | pull req | push req | wasted | wasted share
-------|---------|-------------------|----------|----------|--------|-------------
common |      20 |                12 |      240 |      240 |      0 |        %0.0
unique |   20046 |                 1 |    20046 |   240552 | 220506 |       %91.7

mode   | origin req | pushed obj | wasted | stale version rounds | messages
-------|------------|------------|--------|-----------------------|---------
pull   |      20322 |          0 |      0 |                  1794 |    20322
push   |          0 |     240792 | 220506 |                     0 |   240804
hybrid |      20046 |        240 |      0 |                     0 |    20298

message ratio push/pull = 11.85
stale version rounds, average per edge = 149.5 rounds (0.50 times the TTL)
T12 sensitivity: if the unique object were requested from two edges, wasted share %83.3 (%91.7 at one edge)
```

These numbers belong to the measurement class; their inputs are the T10–T12 assumptions and the
previous lesson's 20,046 measurement.

The first table confirms the selection criterion. In the common class, pull and push request the
same amount (240 to 240) with zero wasted copies, since the object is already requested from all
twelve edges. In the unique class, push sends 240,552 copies, 220,506 of which will never be
requested: a wasted share of 91.7%. The same criterion shows up as a formula — the wasted share is
the complement of the ratio of requesting edges to total edges. Playing T12 drops the share from
91.7% to 83.3%; even doubled, the assumption still leaves push heavily wasted in the unique class.

The second table shows where the cost lands. Pull generates 20,322 requests to the origin and, in
return, serves stale versions for 1,794 rounds — an average of 149.5 rounds per edge, half the
TTL. Push produces no stale versions but sends 240,804 messages, 11.85 times pull's count. The two
models pay for the same work in different currencies: one in stale responses, the other in wasted
messages. This mirrors the tradeoff between the two failover patterns in the introductory course,
where the cost split between downtime and lost data.

The third row shows the decision is made at the content-class level, not the system level. The
hybrid mode pushes common objects and pulls unique ones: 20,046 origin requests, 0 wasted copies,
0 stale-version rounds, 20,298 messages — better than both other modes on all three counts. A
design forced to pick a single model pays unnecessary cost on one of the two classes.

## Back to the Numbers

```js
// propagation/compute.mjs — which of K01's computations change when a propagation mode is chosen
import { propagate, cost, CLASS, EDGES } from "./model.mjs";

const PEAK_READ = 416.67, PEAK_WRITE = 97.22, DAILY_EVENTS = 2_800_000;  // K01 calculation
const V5 = 480, ACTIVE = 1_200_000;             // K01 V5; active shipment count computed with T6
const K01_BEHIND_CACHE = 41.67, K01_WRITE = 0.17, K01_READ = 1.60;
const WINDOW = 480;                             // s: the previous lesson's measurement window

const pull = propagate("pull"), hybrid = propagate("hybrid");
const mbit = (rate, bytes) => (rate * bytes * 8) / 1e6;
const pushRate = PEAK_WRITE * EDGES;
const staleResponses = pull.stale * (PEAK_READ / EDGES);
const pushGB = (ACTIVE * V5 * EDGES) / 1e9, pullMB = (CLASS[1].objects * V5) / 1e6;

console.log(`pull: ${pull.origin} requests to the origin / ${WINDOW} s = ${(pull.origin / WINDOW).toFixed(2)} ` +
  `requests/s -- K01 reads behind cache/s = ${K01_BEHIND_CACHE}`);
console.log(`hybrid: ${(hybrid.origin / WINDOW).toFixed(2)} requests/s to the origin, no cold start ` +
  `miss because the common object is pushed`);
console.log();
console.log(`push: every state event goes to ${EDGES} edges`);
console.log(`  peak push rate = ${pushRate.toFixed(2)} pushes/s, ${DAILY_EVENTS * EDGES} pushes/day`);
console.log(`  push bandwidth = ${mbit(pushRate, V5).toFixed(2)} Mbit/s = ` +
  `${(mbit(pushRate, V5) / K01_WRITE).toFixed(1)} times K01's write ingress (${K01_WRITE}), ` +
  `${(mbit(pushRate, V5) / K01_READ).toFixed(2)} times its read egress (${K01_READ.toFixed(2)})`);
console.log(`  unique content held at the edges = ${pushGB.toFixed(2)} GB ` +
  `(${(ACTIVE * V5 / 1e6).toFixed(0)} MB x ${EDGES} edges)`);
console.log(`  same content in pull mode = ${pullMB.toFixed(2)} MB, ratio ` +
  `${((pushGB * 1e3) / pullMB).toFixed(0)}`);
console.log();
console.log(`stale responses from one version change in pull mode = ` +
  `${staleResponses.toFixed(0)} (${pull.stale} rounds x ${(PEAK_READ / EDGES).toFixed(2)} requests/s)`);
console.log(`unique object's wasted push share = ` +
  `%${(cost(CLASS[1]).wastedShare * 100).toFixed(1)}; a push is only not wasted when the object is ` +
  `requested from all ${EDGES} edges`);
```

```
pull: 20322 requests to the origin / 480 s = 42.34 requests/s -- K01 reads behind cache/s = 41.67
hybrid: 41.76 requests/s to the origin, no cold start miss because the common object is pushed

push: every state event goes to 12 edges
  peak push rate = 1166.64 pushes/s, 33600000 pushes/day
  push bandwidth = 4.48 Mbit/s = 26.4 times K01's write ingress (0.17), 2.80 times its read egress (1.60)
  unique content held at the edges = 6.91 GB (576 MB x 12 edges)
  same content in pull mode = 9.62 MB, ratio 718

stale responses from one version change in pull mode = 62292 (1794 rounds x 34.72 requests/s)
unique object's wasted push share = %91.7; a push is only not wasted when the object is requested from all 12 edges
```

The first line shows pull staying consistent with the introductory course: the request rate to
the origin is 42.34, against the course's `reads behind cache/s` of 41.67. The two numbers come
from separate paths — one from the assumption table, the other from this lesson's model run — and
land within two percent of each other. When pull is chosen, the introductory course's arithmetic
stays valid as it is.

Push breaks that same arithmetic. Since every state event changes the corresponding tracking
response, the new version must go to all twelve edges: peak push rate is 1,166.64 pushes/s,
33,600,000 pushes a day. That bandwidth is 4.48 Mbit/s — 26.4 times the course's `write ingress
Mbit/s` (0.17) and 2.80 times its `read egress Mbit/s` (1.60). Push turns read load into write
load and multiplies it by the edge count. A design's largest bandwidth line item shifts, once push
is chosen, to the write side flowing at 97 events per second.

Storage moves the same way. Holding 1,200,000 active shipments' tracking responses at every edge
costs 576 MB per edge, 6.91 GB across twelve edges. In pull mode, the same content's total
footprint at the edges is 9.62 MB — a 718-fold difference. This figure is not added to the
course's `stored data GB` calculation, because the edge copies do not replace the store — they sit
on top of it.

The last line gives pull's bill. When a common object's version changes, the stale version is
served for 1,794 rounds, and at 34.72 requests per second per edge, that means 62,292 stale
responses. The number ties directly to the TTL and repeats the first lesson's finding: the TTL is
not a cache setting but a single decision that determines both the failover duration and the
version propagation time.

## Summary

- The propagation model is a decision: pull-based distribution pulls content on the first
  request, push-based distribution sends it before any request arrives.
- The selection criterion is how many edges request an object; the wasted push share is the
  complement of the requesting-edge ratio. The measured share was 0% in the common class, 91.7% in
  the unique class.
- Pull generated 20,322 requests to the origin and served stale versions for 1,794 rounds; push
  produced no stale versions but sent 240,804 messages — 11.85 times pull's count.
- Hybrid mode beats both other modes on all three counts (20,046 origin requests, 0 wasted, 0
  stale rounds): the decision is made at the content-class level, not the system level.
- Pull preserves the introductory course's arithmetic: 42.34 requests/s reach the origin, against
  the course's `reads behind cache/s` of 41.67.
- Push breaks it: a peak of 1,166.64 pushes/s and 4.48 Mbit/s — 26.4 times write ingress; content
  held at the edges is 6.91 GB, 718 times pull mode's footprint.

## Next Step

These two lessons kept content at the edge, but the content always came from the same place: the
application itself. The twenty common-class objects — shell, style, script, and icon files — touch
none of the application's logic; they sit as fixed byte sequences and are still requested from the
application processes' address. The next lesson separates that path: it measures what happens to
the requests reaching the application, the number of stops a request touches, and the introductory
course's peak edge request rate once content is served from a place separate from the application.
