---
title: 'Quality of Service'
source: 'https://academia.sh/en/courses/network-operations/quality-of-service'
course: 'Network Operations and Automation'
language: en
updated: '2026-08-17T18:07:16+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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:

```text
# 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

```python
"""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.
