Skip to content
About
Main content

What an overflowing buffer loses

Simulate a modelNeeds Python

Problem

A bounded buffer that fills must decide what to do with the next record: block the , drop the newest (reject the arrival), drop the oldest (prefer fresh data over complete data), or spill. Which records are lost is decided here. When a newer record makes an older one worthless (a price, a sensor reading, a status), dropping the head of the is strictly better than dropping the tail, and for a keyed buffer where only the latest value per key matters, dropping the oldest per key is better still: you reduce volume without losing any entity.

Simulate a stream of updates for 1,000 entities (each record is the new value of one entity, chosen at random). The producer sends 1,500 updates per second for 60 seconds into a buffer of 2,000 records; the applies 1,000 per second, then drains what is left. Run three full-policies: drop-newest, , and drop-oldest per key (a newer update for a key replaces the queued one; only a new key evicts the head). At the end, compare the consumer's value for every entity with the true latest value, and record how old each value was when it was applied.

The script is an exact discrete-event simulation with a fixed seed.

The question. Each policy throws away about the same number of records. How many entities does each leave with a wrong final value, and how stale is what the consumer applies?


Predict

Drop-newest throws away about a third of the records. How many of the 1,000 entities end with a wrong final value?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    what-an-overflowing-buffer-loses.py83 linesDownload
    View the code
    what-an-overflowing-buffer-loses.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """What an overflowing buffer loses: drop-newest, drop-oldest, drop-oldest per key.
    
    A keyed stream (one update per record for one of K entities, such as a product's
    price) overloads a bounded buffer: the producer is faster than the consumer for a
    while, then stops, and the consumer drains what is left. Three full-policies:
    drop-newest (reject the arrival), drop-oldest (evict the head), and drop-oldest per
    key (a newer update for a key replaces the queued one; only a new key evicts the head).
    An exact discrete-event simulation. At the end the consumer's view of every entity is
    compared with the truth.
    """
    import argparse, json, random
    from collections import OrderedDict, deque
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--produce", type=float, default=1500, help="Updates produced per second during the overload.")
    p.add_argument("--consume", type=float, default=1000, help="Updates the consumer applies per second.")
    p.add_argument("--keys", type=int, default=1000, help="Number of entities (keys).")
    p.add_argument("--buffer", type=int, default=2000, help="Buffer capacity (records).")
    p.add_argument("--seconds", type=float, default=60, help="Seconds of overload; then the producer stops and the consumer drains.")
    p.add_argument("--seed", type=int, default=1, help="Random seed for the key sequence.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    
    def run(policy):
        rnd = random.Random(args.seed)
        truth, seen = {}, {}                        # key -> latest value produced / applied
        fifo, keyed = deque(), OrderedDict()        # (key, value, t) for the FIFO policies; key -> (value, t) for per key
        produced = dropped = 0
        ages = []
        t_arr, t_srv, value = 0.0, 0.0, 0
        while True:
            size = len(keyed) if policy == "drop-oldest per key" else len(fifo)
            arriving = t_arr < args.seconds
            if arriving and t_arr <= t_srv:
                t, key = t_arr, rnd.randrange(args.keys)
                value += 1; produced += 1; truth[key] = value
                if policy == "drop-oldest per key":
                    if key in keyed:
                        keyed[key] = (value, t); dropped += 1          # the queued, older update is superseded
                    else:
                        if len(keyed) >= args.buffer: keyed.popitem(last=False); dropped += 1
                        keyed[key] = (value, t)
                elif size < args.buffer:
                    fifo.append((key, value, t))
                elif policy == "drop-newest":
                    dropped += 1
                else:                                                 # drop-oldest
                    fifo.popleft(); fifo.append((key, value, t)); dropped += 1
                t_arr += 1 / args.produce
            elif size:
                t = max(t_srv, 0.0)
                if policy == "drop-oldest per key":
                    key, (v, made) = keyed.popitem(last=False)
                else:
                    key, v, made = fifo.popleft()
                seen[key] = v; ages.append(t - made)
                t_srv = t + 1 / args.consume
            elif arriving:
                t_srv = t_arr
            else:
                break
        ages.sort()
        wrong = sum(1 for k, v in truth.items() if seen.get(k) != v)
        return {"policy": policy, "produced": produced, "dropped": dropped, "dropped_pct": 100 * dropped / produced,
                "wrong_final": wrong, "wrong_final_pct": 100 * wrong / len(truth),
                "p50_age_s": ages[len(ages) // 2], "p99_age_s": ages[int(len(ages) * 0.99)]}
    
    
    rows = [run(pol) for pol in ("drop-newest", "drop-oldest", "drop-oldest per key")]
    for r in rows:
        print(f"{r['policy']:<20} dropped {r['dropped']:>7,} ({r['dropped_pct']:4.1f}%)  entities with a wrong final value "
              f"{r['wrong_final']:>4} ({r['wrong_final_pct']:4.1f}%)  age at delivery p50 {r['p50_age_s']:.3f} s  p99 {r['p99_age_s']:.3f} s")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"policies": rows}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run what-an-overflowing-buffer-loses.py
    uv run what-an-overflowing-buffer-loses.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--produce1500 Updates produced per second during the overload.
--consume1000 Updates the consumer applies per second.
--keys1000 Number of entities (keys).
--buffer2000 Buffer capacity (records).
--seconds60 Seconds of overload; then the producer stops and the consumer drains.
--seed1 Random seed for the key sequence.

Our run

One overload, three full-policies

One overload, three full-policies
Full-policyRecords dropped (%)Entities with a wrong final valueAge when applied, p50Age when applied, p99
drop-newest31.13212 s2 s
drop-oldest31.1441.33 s1.79 s
drop-oldest per key32.90324 ms355 ms
Reference runApple M2 · 8 cores · 8 GB
Output · uv run what-an-overflowing-buffer-loses.py
drop-newest          dropped  28,000 (31.1%)  entities with a wrong final value  321 (32.1%)  age at delivery p50 2.000 s  p99 2.000 s
drop-oldest          dropped  28,000 (31.1%)  entities with a wrong final value   44 ( 4.4%)  age at delivery p50 1.333 s  p99 1.793 s
drop-oldest per key  dropped  29,650 (32.9%)  entities with a wrong final value    0 ( 0.0%)  age at delivery p50 0.324 s  p99 0.355 s
macOS 26.6.2 · Python 3.13.3 · run 2026-10-25

What we found

All three policies threw away roughly a third of the records: 31.1% for drop-newest, 31.1% for drop-oldest and 32.9% superseded for drop-oldest per key. What they lost was very different. Drop-newest left 321 of 1,000 entities with a wrong final value, because the updates it rejected while full included the last one for those entities; drop-oldest left 44; drop-oldest per key left 0. The applied values were also fresher: a median age of 324 ms per key, against 2 s for drop-newest, whose full queue is a fixed delay in front of every record.


Read it

The number of records dropped says almost nothing about the damage. What matters is which records go, and that is the 's decision, made once when the buffer is configured. For state where the latest value wins, coalescing by key keeps the buffer smaller than the number of entities, so it never has to drop an entity at all. For data where every record counts (payments, orders), none of the three dropping policies is acceptable: block the producer or spill instead.