---
title: 'Time and Management Protocols'
source: 'https://academia.sh/en/courses/application-protocols/time-and-management-protocols'
course: 'Application Layer Protocols'
language: en
updated: '2026-08-17T18:07:02+00:00'
license: 'CC BY-SA 4.0'
---

# Time and Management Protocols

Time synchronization's one assumption and the round's asymmetry: if the outbound and return delay are not equal, clock offset follows directly from that gap and the protocol has no field in its hands to see it; alongside this, the round and decision balance of the polling and the notifying management model.

The previous two lessons measured what the intermediary could read from a message. Reading had a
quiet precondition: a cache's freshness lifetime, a certificate's validity interval, a mail's
`Date:` field — all of them assume the two ends' clocks are close to each other.

The protocol that brings clocks close together is the purest example of the course's measurement
axis: it has **four timestamps** in hand and estimates clock offset in a single round. The
estimate rests on a single assumption — **that the outbound delay equals the return delay.** This
lesson's question is what happens when the assumption does not hold, and whether the protocol can
see that it does not.

- **OP51.** The setup is M04/K03's shared setup: `station.example` and a time source. No real time
  server's name is written, no real network call is made.
- **OP52.** The set measured is **40 rounds**. The oracle is known because we produced the setup
  ourselves: the server's clock is **12.500 ms** ahead of the client, and the outbound path is
  **4.000 ms** longer than the return path. The paths jitter round to round, but the asymmetry is
  systematic; the protocol does not see these two numbers, it only sees the four timestamps.
- **OP53.** On the device management side the set is **40 devices** and a **20-tick** window.
  Every device's state changes once inside the window; the measure is whether the manager knows
  the correct state by the end of the window.
- **OP54.** The System Administration course in M03 measured clock drift's **symptoms in logs**;
  that measurement is not repeated. The measure here is synchronization's **own error.**

## Four Timestamps, Two Computations

Clock synchronization is done with a single exchange. Each end writes its own clock the moment it
touches the message; these are the four timestamps NTP carries.

```text
client                                    server (time source)
   |                                        |
   |-- t1: moment the client sends -------->|
   |                                        t2: moment the server receives
   |                                        t3: moment the server sends
   |<---------------------------------------|
   t4: moment the client receives           |

   t1, t4  read on the client's clock
   t2, t3  read on the server's clock
```

This dump is not run; it is the taught form. Two computations follow from these four numbers:

$$
\hat{\theta} = \frac{(t_2 - t_1) + (t_3 - t_4)}{2}
\qquad
\delta = (t_4 - t_1) - (t_3 - t_2)
$$

The first is the **clock offset** estimate, the second is the round's total delay. The second
computation is entirely independent of the clock offset: because the offset enters once as a plus
and once as a minus, it cancels out. The first computation, instead, halves the sum by assuming
**the outbound delay equals the return delay.**

If the assumption does not hold, the error has a closed form. Let the outbound delay be $d_g$ and
the return delay $d_d$; the gap between the estimate and the true offset is exactly:

$$
\hat{\theta} - \theta = \frac{d_g - d_d}{2}
$$

So the error does not come from noise, it comes from **the round's asymmetry**, and it equals
half the gap.

## The Measurement: Asymmetry

```python
SEED = 20260809
OFFSET = 0.0125          # oracle: server clock is this far ahead of the client (s)
ASYMMETRY = 0.0040       # oracle: the outbound path is this much longer than the return path (s)


def generator(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def rounds(count=40, seed=SEED):
    r, out = generator(seed), []
    for i in range(count):
        common = 0.0150 + 0.0001 * r(9)
        outbound = common + ASYMMETRY / 2 + 0.00002 * r(7)
        inbound = common - ASYMMETRY / 2 + 0.00002 * r(7)
        t1 = 100.0 + i
        t2 = t1 + outbound + OFFSET
        t3 = t2 + 0.0002
        t4 = t3 + inbound - OFFSET
        out.append((t1, t2, t3, t4))
    return out


def estimate(d):
    t1, t2, t3, t4 = d
    return ((t2 - t1) + (t3 - t4)) / 2, (t4 - t1) - (t3 - t2)


sample = rounds()
estimates = [estimate(d) for d in sample]
errors = [s - OFFSET for s, _ in estimates]
least_delay = min(estimates, key=lambda x: x[1])
print(f"round {len(sample)}, true offset {OFFSET * 1000:.3f} ms, "
      f"asymmetry {ASYMMETRY * 1000:.3f} ms")
print(f"estimate average {sum(s for s, _ in estimates) / len(sample) * 1000:.4f} ms")
print(f"error: average {sum(errors) / len(errors) * 1000:.4f}, "
      f"smallest {min(errors) * 1000:.4f}, largest {max(errors) * 1000:.4f} ms")
print(f"half the asymmetry {ASYMMETRY / 2 * 1000:.4f} ms")
print(f"least-delay round: estimate {least_delay[0] * 1000:.4f} ms, "
      f"error {(least_delay[0] - OFFSET) * 1000:.4f} ms")

diff = 0.0
for t1, t2, t3, t4 in sample:
    offset_twin = ((t2 - t1) + (t3 - t4)) / 2
    half = ((t4 - t1) - (t3 - t2)) / 2
    diff = max(diff, abs(t1 + half + offset_twin - t2),
               abs(t3 + half - offset_twin - t4))
print(f"largest difference among the timestamps the observational twin produces: {diff:.3e} s")
```

```
round 40, true offset 12.500 ms, asymmetry 4.000 ms
estimate average 14.4917 ms
error: average 1.9917, smallest 1.9400, largest 2.0400 ms
half the asymmetry 2.0000 ms
least-delay round: estimate 14.4700 ms, error 1.9700 ms
largest difference among the timestamps the observational twin produces: 2.842e-14 s
```

## What the Protocol Cannot See

The estimates average 14.4917 ms; the true offset is 12.500 ms; the error is 1.9917 ms, and half
the asymmetry is 2.0000 ms. The smallest error is 1.9400, the largest 2.0400 — all forty rounds sit
**inside a narrow band around two milliseconds.** This is the proof the error is not random: if it
were noise, the errors would scatter on both sides of zero.

The standard improvement is measuring many rounds and picking the **least-delay** one; the round
with the least queuing is considered the cleanest measurement. When that round is picked, the
error drops from 1.9917 to 1.9700 ms. Filtering cuts **noise**, it does not cut **asymmetry** —
because asymmetry is not noise, it is a property of the path itself.

The last line is the crux. A **second world** producing the same four timestamps was constructed:
in it, the path is symmetric and the server's clock really is 14.4917 ms ahead. The two worlds'
timestamps agree down to $2.842 \times 10^{-14}$ seconds — which is the floating-point number's
own resolution. The four timestamps **cannot tell two different truths apart**; the estimate is
not miscomputed, in the twin world it is exactly correct.

Because there is no field to show the asymmetry, this is not even a round debt: no matter how many
rounds are spent, it does not close. Closing it lies outside the protocol — knowing from another
source that the path is symmetric, or placing the timestamps before the path is entered. The
course's fourth reading takes its sharpest form here: **nothing verifies what is written in the
message**; here, what is written in the message is true, and it still is not enough.

## Polling Against Notifying

Device management asks the same axis with a different question: how does a manager learn the
state of forty devices? SNMP defines both models at once. In the **polling** model the manager
asks at regular intervals, and every question is a round trip. In the **notifying** model the
device reports the change itself when it happens; the unacknowledged form does not even cost a
round, the acknowledged form costs one confirmation round.

```python
SEED = 20260809
DEVICES, WINDOW = 40, 20


def generator(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def events():
    r = generator(SEED + 1)
    return [{"device": i + 1, "tick": 1 + r(19), "dropped": r(9) == 0}
            for i in range(DEVICES)]


def poll(interval):
    round_count = correct = delay = 0
    for e in events():
        round_count += WINDOW // interval
        seen = ((e["tick"] - 1) // interval + 1) * interval
        if seen <= WINDOW:
            correct += 1
            delay += seen - e["tick"]
    return round_count, correct, delay / max(correct, 1)


def notify(acked):
    dropped = sum(x["dropped"] for x in events())
    return (DEVICES, DEVICES, 0.0) if acked else (0, DEVICES - dropped, 0.0)


print(f"{'model':<22s} {'round':>5s} {'correct':>6s} {'wrong':>7s} {'delay':>9s}")
for name, (t, d, g) in (("poll, interval 5", poll(5)),
                         ("poll, interval 10", poll(10)),
                         ("poll, interval 20", poll(20)),
                         ("notify, unacked", notify(False)),
                         ("notify, acked", notify(True))):
    print(f"{name:<22s} {t:5d} {d:6d} {DEVICES - d:7d} {g:6.2f} tick")

print(f"\ndevices {DEVICES}, window {WINDOW} ticks, "
      f"notifications dropped in transit {sum(x['dropped'] for x in events())}")
```

```
model                  round correct   wrong     delay
poll, interval 5         160     40       0   2.08 tick
poll, interval 10         80     40       0   4.95 tick
poll, interval 20         40     40       0  10.95 tick
notify, unacked            0     34       6   0.00 tick
notify, acked             40     40       0   0.00 tick

devices 40, window 20 ticks, notifications dropped in transit 6
```

In the polling model, round count and detection lag trade off directly: as the interval rises from
five to twenty, rounds fall from 160 to 40 and delay rises from 2.08 ticks to 10.95. The correct
decision count is forty in all three — polling learns late, but it does not learn wrong.

The notifying model breaks the trade-off. Its acknowledged form gives **the accuracy of the most
frequent polling with 40 rounds instead of 160, and zero delay.** The unacknowledged form drives
round cost to zero and produces a wrong decision on six devices. What matters about these six
wrong decisions is this: the manager **cannot tell silence apart from no change**, and does not
know it is wrong. The acknowledgment round buys exactly this distinction — this lesson's form of
the course's rule is: **an unavailable decision is a round debt, and when the debt goes unpaid, an
assumption takes the decision's place.**

## Summary

- Clock synchronization derives two computations from four timestamps: the delay computation is
  independent of clock offset, while the offset estimate assumes the outbound and return delays
  are equal.
- If the assumption does not hold, the error is not random: across 40 rounds the true offset is
  12.500 ms while the estimates average 14.4917 ms, an error of 1.9917 ms — exactly half the
  asymmetry. Picking the least-delay round only brings the error down to 1.9700 ms; filtering
  removes noise, not asymmetry.
- A symmetric twin world producing the same four timestamps can be constructed; the timestamps
  agree down to floating-point resolution, so the protocol has no field to see the asymmetry, and
  this gap does not close by spending rounds.
- In management, the polling model trades rounds for delay (160 rounds and 2.08 ticks versus 40
  rounds and 10.95 ticks); the acknowledged notifying model gives the same accuracy with 40 rounds
  and zero delay.
- With unacknowledged notification the round count is zero but six devices come out wrong; the
  manager cannot tell silence apart from no change, and the acknowledgment round buys exactly that
  distinction.

## Course Wrap-Up

| lesson | decision measured | taken without a round trip | wrong or unavailable |
|---|---|---|---|
| HTTP 01 · Methods | `repeatable`, `storable` from the method | 195/200 correct, 0 round | 5 wrong: 2 missing flag, 3 validator |
| HTTP 02 · Status codes | caching and retry from the first digit | line regime 309 correct, 4004 bytes | 11 wrong; the body's 6 decisions cost 673.3 bytes/decision |
| HTTP 03 · Headers and negotiation | the split between the resolved field and the carried field | 195 correct; with the full flag 13/13 correct copy | 5 wrong; 9 wrong copies without the flag; 2960 bytes zero decisions |
| HTTP 04 · Cookies | the scope rule and who receives the cached response | 195 correct; scopes carry 11, 24, 40 messages | 2 wrong; with the flag wrapped, 80 decisions move to the endpoints, 40 rounds |
| HTTP 05 · Versions | what framing buys the intermediary | same 195 correct, 440 bytes instead of 3724 | 189 unavailable in a context-less intermediary, 39 rounds |
| HTTP 06 · Caching | the validator's resolution | tag regime 195 correct, 9 of 12 stale copies caught | the tag misses staleness in 3 copies; asking every time costs 40 rounds |
| TLS 01 · Symmetric and asymmetric | the round cost of the role | 0.025 round/exchange on a single persistent connection | 1.000 round on setup for every message |
| TLS 02 · Certificate chain | local verification of the chain | 33/40 correct, 0 round trip; still 0 even as the chain lengthens | 7 wrong, all from revocation; asking costs 111 rounds, same table |
| TLS 03 · Handshake | the round budget and the decision the envelope charges | 40 decisions inside the envelope, none wrong | 160 unavailable, 40 rounds; bytes from 3847 to 920 |
| TLS 04 · Certificate life cycle | revocation being a round debt | a client that never asks pays 0 rounds; all three options in between also have 0 round per exchange | never asking gives 22.02/40 false acceptance; asking on every exchange costs 120 rounds, 0 wrong |
| OTHER 01 · WebSocket | the upgrade being a regime change | the upgrade request gives 2 of 5 decisions | 200 unavailable in the tunnel, 40 rounds, 0 wrong |
| OTHER 02 · Server stream | the stream staying inside HTTP | 1 decision point, 5 decisions, 87 bytes | 195 unavailable, 39 rounds; 73 wrong across scope |
| OTHER 03 · Polling | the round cost per decision | 0.200 round/decision in all four approaches | spent round plus round debt sums to 40 in all of them |
| OTHER 04 · File transfer | the match between the control and data channel | single channel open 195 correct, 0 round | 8 wrong on two channels, 3 from the split; data channel wrapped costs 40 rounds |
| OTHER 05 · Mail | the split between the mail envelope and the header | 60 correct with the envelope, 140 with the header, 180 with the key | 7 wrong from forwarding; unavailable drops from 133 to 13 |
| OTHER 06 · Time and management | round asymmetry and the management model | acked notification 40 correct, 40 rounds, 0 delay | error 1.9917 ms, does not close by adding rounds; 6 wrong when unacked |

Every number in the table is read from that row's own lesson's measurement; no row was filled in
from the shared setup.

The course had one sentence, and sixteen lessons measured it: **whatever is written in the
message, the intermediary knows it; whatever is not written, it does not know; and whatever is
written wrong, it knows wrong.** The measure stayed the same throughout — a protocol's number is
not the fields it carries but how many decisions it lets the receiving side take without an extra
round trip — and every unavailable decision was written down as a round debt.

Three results stand at the close. **Adding a field does not buy a decision:** bytes grow, the
decision table does not change; what buys the gain is not the byte itself, it is being a field
whose name and meaning are fixed. **The cost of hiding is not a defect, it is a choice:** once the
transport envelope closes, the intermediary loses most of the decisions, but there is no wrong
decision among what remains. And **nothing verifies a decision:** the protocol trusts the
declaration, because the declaration is all it has. The last lesson showed the most extreme form
of this — there, the declaration was true and it still was not enough, because what was missing
was not a field but a world.

The intermediary that took all these decisions read the **message** throughout the course. The
next course, Switching and Routing, drops down one layer and looks at the device standing there: a
switch deciding which port to send a frame out of, a router deciding which neighbor to hand a
packet to, and which table each of these decisions is read from. The measure shifts too — here the
intermediary was counted by **what it knew**; there the device will be counted by **how correctly
it forwards.** From the intermediary that takes the decision, to the device that forwards the
packet.
