Advertisement
Open Source Projects by Phil Schwartz

Building a Python-based bandwidth throttler using a token bucket

Phil Schwartz’s personal homepage documents practical open-source tools, including DenyHosts for blocking SSH attacks and Kodos for debugging regular expressions. A bandwidth throttler extends that Linux and Python focus with a small utility, clear behaviour and testable limits.

Australian networks make the project more than a home demonstration. Households compare NBN plans from Telstra, Optus and Aussie Broadband, often around 100/20 Mbps. Sydney and Melbourne commonly have fibre access, while regional Western Australia may rely on fixed wireless or satellite, making upload limits and variable throughput important.

A token bucket smooths traffic without banning short bursts. Picture an arvo download: credits can be spent quickly at the start, then refill gradually. The approach also suits Australian mobile plans sold by the gigabyte, where tethering or a large backup can burn through an included allowance quickly.

This guide develops a Python-based bandwidth throttler using byte costs, a monotonic clock and bounded queues. It is a relay-side prototype rather than a kernel packet filter, with the trade-offs between education and production kept clear.

Why a token bucket fits real networks

A fixed quota answers whether a transfer is allowed, but not how quickly it should proceed. A token bucket controls pace: tokens accumulate at a chosen rate, and each chunk consumes tokens according to its size. When credits run out, the sender waits instead of saturating the link.

This avoids the cliff effects of simple throttling. A 20 Mbps upload on an Australian NBN plan can accept a short burst, then settle into a steady stream. On regional fixed wireless, a small queue protects interactive latency better than filling the buffer until packets drop.

Designing the bucket and its limits

Start with a refill rate in bytes per second and a bucket capacity in bytes. A chunk of size c requires c tokens. If fewer are available, its additional wait is (c − available tokens) divided by the refill rate. Capacity sets the largest burst, not the long-term average.

Choose realistic values rather than copying an ISP headline. An 8 Mbit/s link is roughly 1,000,000 bytes per second before overhead. A 256,000-byte bucket allows a quarter-second burst. Decide whether each connection gets its own bucket or shares credits with others, and whether excess traffic is delayed, dropped or queued.

Implementing the throttle in Python

The standard library is enough for a prototype. A monotonic clock prevents daylight-saving changes or NTP corrections from distorting pacing, while a negative balance records capacity reserved for another transfer.

from dataclasses import dataclass, field
import time

@dataclass
class TokenBucket:
    capacity: int
    rate: int
    tokens: float = 0.0
    updated: float = field(default_factory=time.monotonic)

    def take(self, size: int) -> float:
        now = time.monotonic()
        elapsed = max(0.0, now - self.updated)
        self.tokens = min(
            float(self.capacity),
            self.tokens + elapsed * self.rate,
        )
        self.updated = now
        self.tokens -= size
        return max(0.0, -self.tokens / self.rate)

Call take before writing a chunk, sleep for its delay, then write and drain the socket. Never sleep while holding a lock. This version suits one event loop; add a threading.Lock for multiple workers. JSON files or command-line arguments can supply configuration, with structured logs recording rate, delay and queue depth.

Managing queues, bursts and fairness

A limiter is only as useful as its queue policy. Give each connection its own bucket to stop one transfer consuming every credit, or use weighted scheduling for a shared bucket. Bound the queue and define its overflow behaviour; dropping new chunks is safer than allowing memory use to grow indefinitely.

Payload size is not the whole story on the wire. Ethernet framing, IP and TCP headers consume capacity too, so count transmitted bytes when matching an ISP limit. UDP needs steady pacing, while TCP may react to added delay through congestion control. Measure the result instead of assuming the nominal rate is exact.

Testing the limiter under pressure

Start with unit tests around time. Make the clock injectable and check that an idle bucket fills only to capacity, an oversized chunk produces a sensible delay, and repeated calls cannot create unlimited credits. Cover the first transfer, fractional milliseconds and a zero refill rate.

Then use iperf3 on a lab interface, comparing an unrestricted baseline with several token rates. Record throughput, delay and dropped packets over time. For operational reporting, Phil Schwartz’s Scratchy log analysis tool offers a model for turning raw Apache logs into clear information.

Running the service on Linux

A Python relay can run as an unprivileged user under systemd, with configuration outside the source tree and logs rotated by the operating system. Record queue depth and pacing delays, and expose service health through a simple status endpoint. Keep secrets out of command-line arguments, since process listings can reveal them locally.

For host-wide enforcement, prefer Linux traffic control. Kernel disciplines can shape traffic with a token bucket without placing Python on every packet’s path. The Python version remains useful for learning, custom accounting and controlled relaying, but scheduling accuracy depends on event-loop delays, socket buffers and relayed traffic overhead.

Releasing the project in the open

A small open-source release should include a README, example configuration, tests and a clear licence. An MIT licence would keep this standard-library-only implementation straightforward to reuse without creating a dependency-heavy supply chain. DenyHosts and Kodos show the value of focused tools that solve one operational problem cleanly.

Document the limits as carefully as the code. A relay cannot enforce an ISP’s household policy, and encrypted traffic may only be measured at the byte level. With sensible defaults and honest measurement, a token-bucket throttler can make shared Australian links more predictable for downloads, backups and video calls alike.