Lesson 05 / 16
Quality of Service
Three scheduling rules are run on the same forty subjects, and the 433 units that cannot be served stay the same across all three; strict priority brings the interactive class's wait from 1.13 rounds to 0.00 while sending the standby class from 1.28 to 5.76 rounds and into a 433-unit tail, and the overall average drops from 1.19 to 0.40, appearing to improve.
Contents
The four levers so far all asked the same question in different forms: where should the work go? The balancer to which subject, the proxy on whose behalf, the edge to which copy. None of them accounted for work being able to have different importance; the forty subjects were all waiting in the same line.
Quality of service lifts this assumption. Traffic gets split into classes, one class gets moved ahead, the other gets made to wait. The lever now adjusts not the work’s location but its order. And on a link where capacity is not enough, order is a zero-sum thing: the delay a class gains is taken from another class’s tail. This lesson’s rule is that these two numbers are always written together.
Three Steps
Quality of service is not a single mechanism but three steps that follow each other.
Classification gives every incoming unit a class label. The label can be read from a field in the message, a port, the arriving interface, or a mark placed beforehand in advance. This step makes no decision; it only says who is in which set.
Prioritization builds an order among the classes. It determines which class’s tail gets pulled from when the link frees up. If capacity meets demand, this step has no effect at all; its effect shows up only under congestion.
Shaping caps from above the share a class can take. Its difference from prioritization is this: priority says when a class gets served, shaping says at most how much it can take. Used together, a class can be both prioritized and capped — it moves ahead but cannot swallow the whole link.
None of the three steps creates capacity, and this is quality of service’s most commonly misunderstood side. If the link meets demand, all three steps do nothing at all. The rules’ effect shows up only at the bottleneck; this is why a quality-of-service rule is written not at every point traffic passes through, but at the point where capacity falls short. A rule written somewhere else sits in the configuration, never shows up in the measurement, and gives a false sense of security.
On the configuration side, all three sit in the same place:
# taught rule syntax, not run
classify {
interactive mark = urgent
batch mark = batch
standby remaining
}
schedule {
rule strict-priority
order interactive, batch, standby
}
shape {
interactive at-most 45%
batch at-most 40%
standby at-most 15%
leftover redistribute
}
Classification’s Own Tail
Before moving to the measurement, one warning is needed. The fiction below knows the classes correctly: the forty subjects’ class comes from the fiction itself, and it is the oracle. This is not how it is in operations.
The classifier is a lever too, and it has its own tail. It gives the label by looking at what is written in the message; nothing verifies what is written — the observation left by the Application Layer Protocols course holds here too. When a unit is misclassified, the next two steps produce a wrong result while working flawlessly: the priority rule is applied correctly, shaping is applied correctly, but to the wrong subject. And this leaves no sign in the result itself.
The tail’s two ends are here. If classification is written narrow, traffic that is genuinely urgent falls into the remaining class and gets no benefit from priority at all. If it is written broad, the urgent class swells; prioritization’s meaning depends on the set it sets aside staying small. A schedule where everyone is prioritized is the same as having no schedule.
The measurement’s assumptions:
- TM28 — The forty subjects are the course’s fixed set; each subject’s class comes from the fiction, and classification is taken to be flawless. What is measured is the schedule’s result, not the label’s accuracy.
- TM29 — Twenty rounds are run. In every round, every subject generates a variable demand; demand size depends on the subject’s capacity.
- TM30 — The link carries 150 units per round. Total demand is above this capacity; the number of units that cannot be served is fixed, independent of the rule.
- TM31 — Wait is the difference between the round a unit arrives and the round it gets served, counted in rounds. A unit served in the same round it arrives has a wait of zero.
- TM32 — Under the classless rule there is a single tail, and units get served in arrival order. Under strict priority, the lower class is not touched until the higher class’s tail is empty.
- TM33 — Under shaping, each class is given a per-round cap; capacity left over from the caps is distributed round-robin in a second pass, so the link never sits idle.
- TM34 — The measurement is not a network measurement; no packet is sent. What is counted is the schedule in the fiction itself.
The Measurement
"""Quality of service: one class's gained delay is another class's tail.""" SEED = 20260812 CLASSES = ("interactive", "batch", "standby") ROUNDS, LINK = 20, 150 SHARE = {"interactive": 45, "batch": 40, "standby": 15} def generator(seed): d = seed % 2147483646 + 1 def r(n): nonlocal d d = (d * 48271) % 2147483647 return d % n return r def subjects(count=40, seed=SEED): r, result = generator(seed), [] for i in range(count): result.append({"no": i + 1, "capacity": 20 + r(81), "delay": 5 + r(45), "class": CLASSES[r(3)], "special": r(9) == 0}) return result def demands(subs, seed=SEED + 31): """Every round, every subject generates a variable demand.""" r, stream = generator(seed), [] for _ in range(ROUNDS): rnd = [] for s in subs: for _ in range(1 + r(s["capacity"] // 8 + 1)): rnd.append(s) stream.append(rnd) return stream def run(subs, stream, rule): """A single scheduling rule pushes forty subjects' units through the link.""" tail = {c: [] for c in CLASSES} wait = {c: [] for c in CLASSES} served = {s["no"]: 0 for s in subs} common = [] def pull(items, cap): n = 0 while n < cap and items: arrival, s = items.pop(0) wait[s["class"]].append(t - arrival) served[s["no"]] += 1 n += 1 return n for t in range(ROUNDS): for s in stream[t]: (common if rule == "classless" else tail[s["class"]]).append((t, s)) remaining = LINK if rule == "classless": remaining -= pull(common, remaining) elif rule == "strict-priority": for c in CLASSES: remaining -= pull(tail[c], remaining) else: for c in CLASSES: remaining -= pull(tail[c], min(LINK * SHARE[c] // 100, remaining)) for c in CLASSES: remaining -= pull(tail[c], remaining) for arrival, s in common: tail[s["class"]].append((arrival, s)) return tail, wait, served subs = subjects() stream = demands(subs) print(f"rounds {ROUNDS} | link capacity {LINK}/round, total {ROUNDS * LINK} units | " f"total demand {sum(len(t) for t in stream)} | " f"unservable {sum(len(t) for t in stream) - ROUNDS * LINK}") for c in CLASSES: print(f" {c:12s} subjects {sum(1 for s in subs if s['class'] == c):2d} " f"demand {sum(1 for t in stream for s in t if s['class'] == c):5d}") print() print(f"{'rule':<16s} {'class':<12s} {'served':>7s} {'avg wait':>13s} " f"{'longest':>8s} {'tail':>9s} {'subjects in tail':>16s}") for rule in ("classless", "strict-priority", "shaping"): tail, wait, served = run(subs, stream, rule) for c in CLASSES: w = wait[c] print(f"{rule:<16s} {c:<12s} {len(w):7d} " f"{sum(w) / len(w) if w else 0:13.2f} {max(w) if w else 0:8d} " f"{len(tail[c]):9d} " f"{len({s['no'] for _, s in tail[c]}):16d}") total = [x for c in CLASSES for x in wait[c]] print(f"{'':<16s} {'-- overall':<12s} {len(total):7d} " f"{sum(total) / len(total):13.2f} {max(total):8d} " f"{sum(len(tail[c]) for c in CLASSES):9d} " f"{len({s['no'] for c in CLASSES for _, s in tail[c]}):16d}")
rounds 20 | link capacity 150/round, total 3000 units | total demand 3433 | unservable 433
interactive subjects 11 demand 829
batch subjects 20 demand 1974
standby subjects 9 demand 630
rule class served avg wait longest tail subjects in tail
classless interactive 714 1.13 3 115 11
classless batch 1736 1.19 3 238 20
classless standby 550 1.28 3 80 9
-- overall 3000 1.19 3 433 40
strict-priority interactive 829 0.00 0 0 0
strict-priority batch 1974 0.03 1 0 0
strict-priority standby 197 5.76 13 433 9
-- overall 3000 0.40 13 433 9
shaping interactive 829 0.00 0 0 0
shaping batch 1731 1.14 3 243 20
shaping standby 440 2.85 7 190 9
-- overall 3000 1.08 7 433 29
Taken From Whom
The first column of the table that should be read is not the rightmost one, but the three
-- overall rows’ tail value. All three are 433. Total demand is 3433, the link
carries 3000 units over twenty rounds, and the gap between them closes with no rule.
This is this topic’s last form of the course’s second claim. A scheduling rule does not destroy the unservable unit; it chooses who carries it. None of the three rules grew the link, none shrank the demand; they only distributed the same 433 units over different subjects.
The distribution itself reads directly. Under the classless rule, the tail splits three ways: interactive 115, batch 238, standby 80. Moving to strict priority zeroes out interactive’s 115 and batch’s 238, and both pile onto the standby class: 80 + 115 + 238 = 433. The standby class’s tail climbs from 80 to 433, and only 197 of that class’s 630-unit demand gets served.
The same arithmetic shows up in the wait, in rounds. The interactive class’s average wait drops from 1.13 rounds to 0.00 — not a single one of its units waits at all, and its longest wait is also 0. The gain is exactly 1.13 rounds. In the same move, the standby class’s average wait climbs from 1.28 to 5.76 rounds, its longest wait from 3 to 13. The 1.13 rounds interactive gains is 4.48 rounds standby pays. Written separately, both numbers are correct; written alone, the rule looks either flawless or catastrophic.
The shaping rows give the middle ground, and show that it is exactly that — a middle ground. Interactive again waits 0.00 — the upper class’s gain is preserved. But standby’s wait is 2.85 instead of 5.76, its tail 190 instead of 433. The difference is the share the batch class takes on: its tail climbs from 238 to 243, and its wait drops from 1.19 to 1.14. The same 433 units are split this time as 243 + 190.
What shaping does is spread the load strict priority dumps on a single class across two classes. It does not remove the tail; it adjusts who the tail accumulates on — the same result that has come out of every lever since the start of the course.
Waiting Versus Dropping
The shaping in the measurement waited the units above the cap: the unit stayed in the tail and got served when its turn came. This is only one of two options. The second is dropping the units above the cap — in this course this is called policing. The lever is the same lever, the cap is the same cap; the only thing that changes is what happens to the unit above the cap.
The distinction’s effect on the tail is direct. The waiting rule holds the unservable 433 units in the tail; these units get served, late as it is, and show up in the table as wait time. The dropping rule never shows them at all: the tail column reads 0, wait times drop, the gauge looks clean. The lost units are in no column at all.
On top of that, dropping may not eliminate the dropped unit. If an upper layer notices the loss and resends, the same job comes to the link a second time — this time later and with extra load. The retransmission behavior measured in the Transport Layer topic of the Network Models and Protocols course becomes an operations problem here: enforcing the cap by dropping can raise demand instead of lowering it.
There is a limit in the opposite direction too. Waiting may always look better, but the tail itself is a cost. As the tail deepens, wait grows, and in a deep shared tail, an urgent unit sits behind a large number of batch units that arrived before it. The classless rule in the measurement is exactly this situation: the interactive class’s 1.13-round wait comes not from its own traffic, but from other classes’ units ahead of it. This is also classification’s reason for existing — without splitting the tail, the tail’s depth charges everyone the same cost.
What the Overall Average Does Not Say
The last reading is in the -- overall rows, and it is a dashboard trap.
Overall average wait is 1.19 rounds under the classless rule, 0.40 rounds under strict priority. A gauge that looks only at this number would say moving to strict priority improves overall delay by more than threefold. The number is not wrong: the average across the three thousand units served genuinely dropped, because most of the weight now sits in two classes that never wait at all.
The two columns next to the same row say the opposite. Longest wait climbs from 3 rounds to 13; the number of subjects carrying a tail drops from 40 to 9. Forty subjects waiting a little turns into nine subjects waiting a lot, and this is exactly why the average drops.
The 40 → 9 drop in the subjects-in-tail column can read, on its own, like an improvement — the sentence “the number of subjects waiting in the tail fell to a quarter” is true and completely misleading. In a set of forty subjects, the smallest measurable difference is 1/40 = 0.025; the change here is thirty-one times that, and its direction is set by policy, not by the measure.
The shaping row softens this trap too: overall average 1.08, longest wait 7, subjects carrying a tail 29. None of the three numbers is the best, but all three together leave no single class alone. Which row gets chosen is not the measurement’s decision — the measurement only says who pays.
Summary
- Quality of service is three steps: classification gives the label, prioritization builds the order, shaping sets the cap; if the first step is wrong, the next two produce a wrong result while working flawlessly.
- Total demand is 3433, link capacity 3000; the unservable 433 units are the same across all three rules. A scheduling rule does not destroy the tail, it chooses who carries it.
- Under strict priority, interactive’s 115-unit and batch’s 238-unit tails pile onto the standby class: 80 + 115 + 238 = 433, and only 197 of standby’s 630-unit demand gets served.
- The 1.13-round wait interactive gains is 4.48 rounds standby pays; if the two numbers are not written together, the same rule looks either flawless or catastrophic.
- Shaping splits the same 433 units into two classes as 243 + 190; it preserves the upper class’s 0.00 gain but holds the lower class’s wait at 2.85 instead of 5.76.
- The overall average drops from 1.19 to 0.40 under strict priority — while at the same time the longest wait climbs from 3 to 13 and subjects carrying a tail fall from 40 to 9.
Next Step
In these five lessons, the lever genuinely managed traffic: it distributed, it limited, it placed, it scheduled. In every lesson we could also count the tail — how many subjects overran, how many units sat idle, how many objects were never seen, who paid whose delay. But there was a single reason we could write down all of these numbers: we built the fiction ourselves, and the oracle was in our hands. We knew capacity, popularity, class, and real demand from the start.
None of these are known in operations. As seen in the last lesson, the lever’s own gauge can say it improved while the tail grew, and the gauge is not lying. So the next question is not about the lever: how do we know what the lever is doing? The next topic takes on this question, and starts from its most basic point — it defines which records, which counters, the network reports its own state with.
To keep your progress and take notes, Log in
My notes
Log in to take notes.