The bucket starts full. Each packet you send costs one token; tokens refill automatically at the configured rate. Try sending a burst — the bucket drains fast. Then wait and watch it recover.
<div class="controls">
<label>Bucket capacity: <strong id="capVal">10</strong> tokens
<input type="range" id="capSlider" min="4" max="20" value="10">
</label>
<label>Refill rate: <strong id="rateVal">2</strong> tokens/s
<input type="range" id="rateSlider" min="1" max="8" value="2">
</label>
</div>
<div class="bucket-wrap">
<div class="bucket-outer">
<div class="bucket-fill" id="fill"></div>
<div class="bucket-label" id="bucketLabel">10 / 10</div>
</div>
<div class="legend">
<div class="legend-row"><span class="dot green"></span> Token available</div>
<div class="legend-row"><span class="dot red"></span> Packet dropped</div>
</div>
</div>
<div class="log" id="log"></div>
<div class="btns">
<button id="sendOne">Send 1 packet</button>
<button id="sendBurst">Send burst (5 packets)</button>
<button id="resetBtn" class="ghost">Reset</button>
</div>
<div class="stats" id="stats">Sent: 0 | Dropped: 0 | Tokens refilled: 0</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; font-size: .88rem; }
.controls label { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.controls input[type=range] { flex: 1; min-width: 100px; max-width: 180px; }
.bucket-wrap { display: flex; align-items: flex-end; gap: 1.4rem; margin-bottom: .7rem; }
.bucket-outer { position: relative; width: 72px; height: 130px; border: 2.5px solid #1d3557;
border-radius: 0 0 10px 10px; overflow: hidden; background: #f0f4f8; }
.bucket-fill { position: absolute; bottom: 0; left: 0; right: 0; background: #2a9d8f;
transition: height .35s ease; }
.bucket-label { position: absolute; width: 100%; text-align: center; bottom: 6px;
font: 700 12px ui-monospace, monospace; color: #fff; text-shadow: 0 1px 2px #0006; pointer-events: none; }
.legend { font-size: .82rem; display: flex; flex-direction: column; gap: .3rem; }
.legend-row { display: flex; align-items: center; gap: .4rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.dot.green { background: #2a9d8f; }
.dot.red { background: #e63946; }
.log { height: 90px; overflow-y: auto; background: #f8f9fa; border: 1px solid #dde;
border-radius: 6px; padding: .4rem .6rem; font: 13px ui-monospace, monospace;
margin-bottom: .6rem; }
.log .ok { color: #0a7d33; }
.log .drop { color: #c92f3c; }
.log .refill { color: #1d6fa5; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .83rem; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Token Bucket simulation.
// State: tokens (float), capacity (int), rate (tokens/sec).
// Invariant: tokens in [0, capacity].
// Per packet: if tokens >= 1, consume 1 and pass; else drop.
// Refill: tokens = min(capacity, tokens + rate * dt) each animation frame.
let capacity = 10, rate = 2;
let tokens = capacity;
let sent = 0, dropped = 0, totalRefilled = 0;
let lastTick = performance.now();
let rafId = null;
const fillEl = document.getElementById('fill');
const labelEl = document.getElementById('bucketLabel');
const logEl = document.getElementById('log');
const statsEl = document.getElementById('stats');
const capSlider = document.getElementById('capSlider');
const rateSlider = document.getElementById('rateSlider');
const capVal = document.getElementById('capVal');
const rateVal = document.getElementById('rateVal');
function updateBucket() {
const pct = tokens / capacity;
fillEl.style.height = (pct * 100) + '%';
labelEl.textContent = tokens.toFixed(1) + ' / ' + capacity;
}
function addLog(text, cls) {
const d = document.createElement('div');
d.className = cls; d.textContent = text;
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
// keep log bounded
while (logEl.children.length > 60) logEl.removeChild(logEl.firstChild);
}
function updateStats() {
statsEl.innerHTML = fmt('Sent: %sent% | Dropped: %dropped% | Tokens refilled: %round%', { sent: sent, dropped: dropped, round: Math.round(totalRefilled) });
}
function tick(now) {
const dt = (now - lastTick) / 1000;
lastTick = now;
const add = rate * dt;
const before = tokens;
tokens = Math.min(capacity, tokens + add);
const actual = tokens - before;
if (actual > 0.001) {
totalRefilled += actual;
// log a refill message only roughly once per token
if (Math.floor(tokens) > Math.floor(before)) {
addLog(fmt('↑ Refill — tokens now %tokens%', { tokens: tokens.toFixed(1) }), 'refill');
}
}
updateBucket();
updateStats();
rafId = requestAnimationFrame(tick);
}
function sendPackets(n) {
for (let i = 0; i < n; i++) {
if (tokens >= 1) {
tokens -= 1;
sent++;
addLog(fmt('✓ Packet sent — %tokens% tokens left', { tokens: tokens.toFixed(1) }), 'ok');
} else {
dropped++;
addLog('✗ Packet DROPPED — bucket empty!', 'drop');
}
}
updateBucket();
updateStats();
}
document.getElementById('sendOne').addEventListener('click', () => sendPackets(1));
document.getElementById('sendBurst').addEventListener('click', () => sendPackets(5));
document.getElementById('resetBtn').addEventListener('click', () => {
tokens = capacity; sent = 0; dropped = 0; totalRefilled = 0;
logEl.innerHTML = '';
updateBucket(); updateStats();
addLog('— Reset —', 'refill');
});
capSlider.addEventListener('input', () => {
capacity = parseInt(capSlider.value);
capVal.textContent = capacity;
tokens = Math.min(tokens, capacity);
updateBucket();
});
rateSlider.addEventListener('input', () => {
rate = parseInt(rateSlider.value);
rateVal.textContent = rate;
});
// Verify core invariant in console (node-verifiable subset):
// For any sequence of send(n) and wait(t) calls:
// total_passed <= initial_tokens + rate * total_time
// Test: capacity=10, rate=2, drain all 10, wait 3s => 6 more tokens pass
// => total = 16 <= 10 + 2*3 = 16 ✓
console.log(fmt('Token bucket demo loaded. capacity=%capacity% rate=%rate% tokens/s', { capacity: capacity, rate: rate }));
console.log('Invariant: packets_passed <= capacity + rate * elapsed_seconds');
addLog(fmt('Bucket full (%capacity% tokens). Ready.', { capacity: capacity }), 'refill');
updateBucket(); updateStats();
lastTick = performance.now();
rafId = requestAnimationFrame(tick);
<div class="controls">
<label>Bucket capacity: <strong id="capVal">10</strong> tokens
<input type="range" id="capSlider" min="4" max="20" value="10">
</label>
<label>Refill rate: <strong id="rateVal">2</strong> tokens/s
<input type="range" id="rateSlider" min="1" max="8" value="2">
</label>
</div>
<div class="bucket-wrap">
<div class="bucket-outer">
<div class="bucket-fill" id="fill"></div>
<div class="bucket-label" id="bucketLabel">10 / 10</div>
</div>
<div class="legend">
<div class="legend-row"><span class="dot green"></span> Token available</div>
<div class="legend-row"><span class="dot red"></span> Packet dropped</div>
</div>
</div>
<div class="log" id="log"></div>
<div class="btns">
<button id="sendOne">Send 1 packet</button>
<button id="sendBurst">Send burst (5 packets)</button>
<button id="resetBtn" class="ghost">Reset</button>
</div>
<div class="stats" id="stats">Sent: 0 | Dropped: 0 | Tokens refilled: 0</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; font-size: .88rem; }
.controls label { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.controls input[type=range] { flex: 1; min-width: 100px; max-width: 180px; }
.bucket-wrap { display: flex; align-items: flex-end; gap: 1.4rem; margin-bottom: .7rem; }
.bucket-outer { position: relative; width: 72px; height: 130px; border: 2.5px solid #1d3557;
border-radius: 0 0 10px 10px; overflow: hidden; background: #f0f4f8; }
.bucket-fill { position: absolute; bottom: 0; left: 0; right: 0; background: #2a9d8f;
transition: height .35s ease; }
.bucket-label { position: absolute; width: 100%; text-align: center; bottom: 6px;
font: 700 12px ui-monospace, monospace; color: #fff; text-shadow: 0 1px 2px #0006; pointer-events: none; }
.legend { font-size: .82rem; display: flex; flex-direction: column; gap: .3rem; }
.legend-row { display: flex; align-items: center; gap: .4rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.dot.green { background: #2a9d8f; }
.dot.red { background: #e63946; }
.log { height: 90px; overflow-y: auto; background: #f8f9fa; border: 1px solid #dde;
border-radius: 6px; padding: .4rem .6rem; font: 13px ui-monospace, monospace;
margin-bottom: .6rem; }
.log .ok { color: #0a7d33; }
.log .drop { color: #c92f3c; }
.log .refill { color: #1d6fa5; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .83rem; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Token Bucket simulation.
// State: tokens (float), capacity (int), rate (tokens/sec).
// Invariant: tokens in [0, capacity].
// Per packet: if tokens >= 1, consume 1 and pass; else drop.
// Refill: tokens = min(capacity, tokens + rate * dt) each animation frame.
let capacity = 10, rate = 2;
let tokens = capacity;
let sent = 0, dropped = 0, totalRefilled = 0;
let lastTick = performance.now();
let rafId = null;
const fillEl = document.getElementById('fill');
const labelEl = document.getElementById('bucketLabel');
const logEl = document.getElementById('log');
const statsEl = document.getElementById('stats');
const capSlider = document.getElementById('capSlider');
const rateSlider = document.getElementById('rateSlider');
const capVal = document.getElementById('capVal');
const rateVal = document.getElementById('rateVal');
function updateBucket() {
const pct = tokens / capacity;
fillEl.style.height = (pct * 100) + '%';
labelEl.textContent = tokens.toFixed(1) + ' / ' + capacity;
}
function addLog(text, cls) {
const d = document.createElement('div');
d.className = cls; d.textContent = text;
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
// keep log bounded
while (logEl.children.length > 60) logEl.removeChild(logEl.firstChild);
}
function updateStats() {
statsEl.innerHTML = fmt('Sent: %sent% | Dropped: %dropped% | Tokens refilled: %round%', { sent: sent, dropped: dropped, round: Math.round(totalRefilled) });
}
function tick(now) {
const dt = (now - lastTick) / 1000;
lastTick = now;
const add = rate * dt;
const before = tokens;
tokens = Math.min(capacity, tokens + add);
const actual = tokens - before;
if (actual > 0.001) {
totalRefilled += actual;
// log a refill message only roughly once per token
if (Math.floor(tokens) > Math.floor(before)) {
addLog(fmt('↑ Refill — tokens now %tokens%', { tokens: tokens.toFixed(1) }), 'refill');
}
}
updateBucket();
updateStats();
rafId = requestAnimationFrame(tick);
}
function sendPackets(n) {
for (let i = 0; i < n; i++) {
if (tokens >= 1) {
tokens -= 1;
sent++;
addLog(fmt('✓ Packet sent — %tokens% tokens left', { tokens: tokens.toFixed(1) }), 'ok');
} else {
dropped++;
addLog('✗ Packet DROPPED — bucket empty!', 'drop');
}
}
updateBucket();
updateStats();
}
document.getElementById('sendOne').addEventListener('click', () => sendPackets(1));
document.getElementById('sendBurst').addEventListener('click', () => sendPackets(5));
document.getElementById('resetBtn').addEventListener('click', () => {
tokens = capacity; sent = 0; dropped = 0; totalRefilled = 0;
logEl.innerHTML = '';
updateBucket(); updateStats();
addLog('— Reset —', 'refill');
});
capSlider.addEventListener('input', () => {
capacity = parseInt(capSlider.value);
capVal.textContent = capacity;
tokens = Math.min(tokens, capacity);
updateBucket();
});
rateSlider.addEventListener('input', () => {
rate = parseInt(rateSlider.value);
rateVal.textContent = rate;
});
// Verify core invariant in console (node-verifiable subset):
// For any sequence of send(n) and wait(t) calls:
// total_passed <= initial_tokens + rate * total_time
// Test: capacity=10, rate=2, drain all 10, wait 3s => 6 more tokens pass
// => total = 16 <= 10 + 2*3 = 16 ✓
console.log(fmt('Token bucket demo loaded. capacity=%capacity% rate=%rate% tokens/s', { capacity: capacity, rate: rate }));
console.log('Invariant: packets_passed <= capacity + rate * elapsed_seconds');
addLog(fmt('Bucket full (%capacity% tokens). Ready.', { capacity: capacity }), 'refill');
updateBucket(); updateStats();
lastTick = performance.now();
rafId = requestAnimationFrame(tick);
Notice the asymmetry between sending and refilling. You can drain the bucket almost instantly with a burst, but refilling takes real time proportional to the rate. This is the key invariant: no matter how spiky the traffic, the number of packets passing through over any interval T can never exceed capacity+rate×T. That bound is provably tight — it is why network engineers can give latency guarantees even on shared links.
Comments
Loading comments...