Click Send burst to inject a batch of packets into the bucket. The bucket drains at a constant rate — one packet every tick. If packets arrive faster than the drain rate, the excess overflows and is dropped. Adjust the burst size and watch the effect.
<!-- Leaky Bucket demo — burst packets in, constant drain out -->
<div class="controls">
<label for="burstSize">Burst size: <span id="burstVal">8</span></label>
<input id="burstSize" type="range" min="1" max="20" value="8">
<button id="btnBurst" type="button">Send burst</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="viz">
<div class="bucket-wrap">
<div class="bucket-label">Bucket</div>
<div class="bucket" id="bucket">
<div class="fill" id="fill"></div>
<div class="drain-arrow" id="drainArrow">▼</div>
</div>
<div class="bucket-stats">
<span>Queued: <b id="queued">0</b></span>
<span>Capacity: <b id="capDisp">16</b></span>
</div>
</div>
<div class="stream-wrap">
<div class="stream-label">Output stream</div>
<canvas id="streamCanvas" width="260" height="120"></canvas>
</div>
</div>
<div class="metrics">
<div class="metric"><span class="mval" id="mSent">0</span><span class="mlbl">Sent</span></div>
<div class="metric"><span class="mval" id="mFwd">0</span><span class="mlbl">Forwarded</span></div>
<div class="metric drop"><span class="mval" id="mDrop">0</span><span class="mlbl">Dropped</span></div>
</div>
<div class="status" id="status">Click "Send burst" to inject packets.</div>
/* Leaky Bucket demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-size: .88rem; color: #444; }
input[type=range] { width: 110px; cursor: pointer; }
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; }
.viz { display: flex; gap: 1.2rem; align-items: flex-start; flex-wrap: wrap; }
/* Bucket visual — fills from bottom, drains via arrow indicator */
.bucket-wrap { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.bucket-label, .stream-label { font-size: .8rem; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: .04em; }
.bucket { position: relative; width: 80px; height: 120px; border: 2.5px solid #1d3557;
border-top: none; border-radius: 0 0 14px 14px; overflow: hidden; background: #eef2f7; }
.fill { position: absolute; bottom: 0; left: 0; right: 0; background: #457bb7;
transition: height .22s ease; height: 0%; }
.drain-arrow { position: absolute; bottom: -22px; left: 50%; transform: translateX(-50%);
font-size: 1.1rem; color: #457bb7; opacity: 0; transition: opacity .15s; }
.drain-arrow.active { opacity: 1; }
.bucket-stats { font-size: .8rem; color: #444; display: flex; flex-direction: column; gap: .1rem; }
/* Output stream canvas and metric counters */
.stream-wrap { flex: 1; min-width: 200px; display: flex; flex-direction: column; gap: .3rem; }
canvas { border: 1px solid #dde4ec; border-radius: 8px; background: #f8fafc; display: block; }
.metrics { display: flex; gap: 1.2rem; margin-top: .6rem; flex-wrap: wrap; }
.metric { display: flex; flex-direction: column; align-items: center; gap: .1rem; }
.mval { font-size: 1.5rem; font-weight: 700; color: #1d3557; }
.metric.drop .mval { color: #c92f3c; }
.mlbl { font-size: .75rem; color: #666; }
.status { margin-top: .5rem; font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #444; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Algorithm parameters
const CAPACITY = 16;
const DRAIN_RATE = 1; // packets forwarded per tick
const TICK_MS = 600;
let bucket = 0; // packets currently queued in the bucket
let totalSent = 0;
let totalFwd = 0;
let totalDrop = 0;
let history = []; // per-tick forwarded counts for the bar chart
let running = false;
let intervalId = null;
const canvas = document.getElementById('streamCanvas');
const ctx = canvas.getContext('2d');
function drawStream() {
// Draw one bar per tick; bar height = forwarded packets that tick
ctx.clearRect(0, 0, canvas.width, canvas.height);
const maxBars = 26;
const barW = 8;
const gap = 2;
const baseY = canvas.height - 10;
const scaleH = (canvas.height - 20);
const recent = history.slice(-maxBars);
recent.forEach((val, i) => {
const x = canvas.width - (recent.length - i) * (barW + gap) + gap;
if (val > 0) {
ctx.fillStyle = '#457bb7';
ctx.fillRect(x, baseY - scaleH * Math.min(val / DRAIN_RATE, 1), barW, scaleH * Math.min(val / DRAIN_RATE, 1));
}
});
// Red dashed line marks the maximum output rate (DRAIN_RATE)
ctx.strokeStyle = '#c92f3c';
ctx.setLineDash([4, 3]);
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, baseY - scaleH);
ctx.lineTo(canvas.width, baseY - scaleH);
ctx.stroke();
ctx.setLineDash([]);
}
function updateUI() {
const pct = (bucket / CAPACITY) * 100;
document.getElementById('fill').style.height = pct + '%';
document.getElementById('queued').textContent = bucket;
document.getElementById('capDisp').textContent = CAPACITY;
document.getElementById('mSent').textContent = totalSent;
document.getElementById('mFwd').textContent = totalFwd;
document.getElementById('mDrop').textContent = totalDrop;
drawStream();
}
function tick() {
// Drain up to DRAIN_RATE packets from the bucket each tick
let forwarded = 0;
if (bucket > 0) {
const drained = Math.min(bucket, DRAIN_RATE);
bucket -= drained;
forwarded = drained;
totalFwd += forwarded;
document.getElementById('drainArrow').classList.add('active');
} else {
document.getElementById('drainArrow').classList.remove('active');
}
history.push(forwarded);
if (history.length > 60) history.shift();
updateUI();
if (bucket === 0 && !running) {
document.getElementById('status').textContent = i18nFmt('Bucket empty. Forwarded %fwd%, dropped %drop% total.', { fwd: totalFwd, drop: totalDrop });
}
}
function sendBurst() {
const burst = parseInt(document.getElementById('burstSize').value, 10);
totalSent += burst;
const accepted = Math.min(burst, CAPACITY - bucket);
const dropped = burst - accepted;
bucket += accepted;
totalDrop += dropped;
running = true;
if (!intervalId) {
intervalId = setInterval(() => {
tick();
if (bucket === 0) {
running = false;
}
}, TICK_MS);
}
document.getElementById('status').textContent = i18nFmt('Injected %n% packets — %drop% dropped (overflow).', { n: burst, drop: dropped });
updateUI();
}
function reset() {
// Clear all state and stop the timer
clearInterval(intervalId);
intervalId = null;
bucket = 0;
totalSent = 0;
totalFwd = 0;
totalDrop = 0;
history = [];
running = false;
document.getElementById('drainArrow').classList.remove('active');
document.getElementById('status').textContent = 'Click "Send burst" to inject packets.';
updateUI();
}
document.getElementById('burstSize').addEventListener('input', function () {
document.getElementById('burstVal').textContent = this.value;
});
document.getElementById('btnBurst').addEventListener('click', sendBurst);
document.getElementById('btnReset').addEventListener('click', reset);
// Initial render with empty bucket
updateUI();
<!-- Leaky Bucket demo — burst packets in, constant drain out -->
<div class="controls">
<label for="burstSize">Burst size: <span id="burstVal">8</span></label>
<input id="burstSize" type="range" min="1" max="20" value="8">
<button id="btnBurst" type="button">Send burst</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="viz">
<div class="bucket-wrap">
<div class="bucket-label">Bucket</div>
<div class="bucket" id="bucket">
<div class="fill" id="fill"></div>
<div class="drain-arrow" id="drainArrow">▼</div>
</div>
<div class="bucket-stats">
<span>Queued: <b id="queued">0</b></span>
<span>Capacity: <b id="capDisp">16</b></span>
</div>
</div>
<div class="stream-wrap">
<div class="stream-label">Output stream</div>
<canvas id="streamCanvas" width="260" height="120"></canvas>
</div>
</div>
<div class="metrics">
<div class="metric"><span class="mval" id="mSent">0</span><span class="mlbl">Sent</span></div>
<div class="metric"><span class="mval" id="mFwd">0</span><span class="mlbl">Forwarded</span></div>
<div class="metric drop"><span class="mval" id="mDrop">0</span><span class="mlbl">Dropped</span></div>
</div>
<div class="status" id="status">Click "Send burst" to inject packets.</div>
/* Leaky Bucket demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-size: .88rem; color: #444; }
input[type=range] { width: 110px; cursor: pointer; }
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; }
.viz { display: flex; gap: 1.2rem; align-items: flex-start; flex-wrap: wrap; }
/* Bucket visual — fills from bottom, drains via arrow indicator */
.bucket-wrap { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.bucket-label, .stream-label { font-size: .8rem; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: .04em; }
.bucket { position: relative; width: 80px; height: 120px; border: 2.5px solid #1d3557;
border-top: none; border-radius: 0 0 14px 14px; overflow: hidden; background: #eef2f7; }
.fill { position: absolute; bottom: 0; left: 0; right: 0; background: #457bb7;
transition: height .22s ease; height: 0%; }
.drain-arrow { position: absolute; bottom: -22px; left: 50%; transform: translateX(-50%);
font-size: 1.1rem; color: #457bb7; opacity: 0; transition: opacity .15s; }
.drain-arrow.active { opacity: 1; }
.bucket-stats { font-size: .8rem; color: #444; display: flex; flex-direction: column; gap: .1rem; }
/* Output stream canvas and metric counters */
.stream-wrap { flex: 1; min-width: 200px; display: flex; flex-direction: column; gap: .3rem; }
canvas { border: 1px solid #dde4ec; border-radius: 8px; background: #f8fafc; display: block; }
.metrics { display: flex; gap: 1.2rem; margin-top: .6rem; flex-wrap: wrap; }
.metric { display: flex; flex-direction: column; align-items: center; gap: .1rem; }
.mval { font-size: 1.5rem; font-weight: 700; color: #1d3557; }
.metric.drop .mval { color: #c92f3c; }
.mlbl { font-size: .75rem; color: #666; }
.status { margin-top: .5rem; font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #444; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Algorithm parameters
const CAPACITY = 16;
const DRAIN_RATE = 1; // packets forwarded per tick
const TICK_MS = 600;
let bucket = 0; // packets currently queued in the bucket
let totalSent = 0;
let totalFwd = 0;
let totalDrop = 0;
let history = []; // per-tick forwarded counts for the bar chart
let running = false;
let intervalId = null;
const canvas = document.getElementById('streamCanvas');
const ctx = canvas.getContext('2d');
function drawStream() {
// Draw one bar per tick; bar height = forwarded packets that tick
ctx.clearRect(0, 0, canvas.width, canvas.height);
const maxBars = 26;
const barW = 8;
const gap = 2;
const baseY = canvas.height - 10;
const scaleH = (canvas.height - 20);
const recent = history.slice(-maxBars);
recent.forEach((val, i) => {
const x = canvas.width - (recent.length - i) * (barW + gap) + gap;
if (val > 0) {
ctx.fillStyle = '#457bb7';
ctx.fillRect(x, baseY - scaleH * Math.min(val / DRAIN_RATE, 1), barW, scaleH * Math.min(val / DRAIN_RATE, 1));
}
});
// Red dashed line marks the maximum output rate (DRAIN_RATE)
ctx.strokeStyle = '#c92f3c';
ctx.setLineDash([4, 3]);
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, baseY - scaleH);
ctx.lineTo(canvas.width, baseY - scaleH);
ctx.stroke();
ctx.setLineDash([]);
}
function updateUI() {
const pct = (bucket / CAPACITY) * 100;
document.getElementById('fill').style.height = pct + '%';
document.getElementById('queued').textContent = bucket;
document.getElementById('capDisp').textContent = CAPACITY;
document.getElementById('mSent').textContent = totalSent;
document.getElementById('mFwd').textContent = totalFwd;
document.getElementById('mDrop').textContent = totalDrop;
drawStream();
}
function tick() {
// Drain up to DRAIN_RATE packets from the bucket each tick
let forwarded = 0;
if (bucket > 0) {
const drained = Math.min(bucket, DRAIN_RATE);
bucket -= drained;
forwarded = drained;
totalFwd += forwarded;
document.getElementById('drainArrow').classList.add('active');
} else {
document.getElementById('drainArrow').classList.remove('active');
}
history.push(forwarded);
if (history.length > 60) history.shift();
updateUI();
if (bucket === 0 && !running) {
document.getElementById('status').textContent = i18nFmt('Bucket empty. Forwarded %fwd%, dropped %drop% total.', { fwd: totalFwd, drop: totalDrop });
}
}
function sendBurst() {
const burst = parseInt(document.getElementById('burstSize').value, 10);
totalSent += burst;
const accepted = Math.min(burst, CAPACITY - bucket);
const dropped = burst - accepted;
bucket += accepted;
totalDrop += dropped;
running = true;
if (!intervalId) {
intervalId = setInterval(() => {
tick();
if (bucket === 0) {
running = false;
}
}, TICK_MS);
}
document.getElementById('status').textContent = i18nFmt('Injected %n% packets — %drop% dropped (overflow).', { n: burst, drop: dropped });
updateUI();
}
function reset() {
// Clear all state and stop the timer
clearInterval(intervalId);
intervalId = null;
bucket = 0;
totalSent = 0;
totalFwd = 0;
totalDrop = 0;
history = [];
running = false;
document.getElementById('drainArrow').classList.remove('active');
document.getElementById('status').textContent = 'Click "Send burst" to inject packets.';
updateUI();
}
document.getElementById('burstSize').addEventListener('input', function () {
document.getElementById('burstVal').textContent = this.value;
});
document.getElementById('btnBurst').addEventListener('click', sendBurst);
document.getElementById('btnReset').addEventListener('click', reset);
// Initial render with empty bucket
updateUI();
Notice the key property: no matter how large or irregular the incoming bursts are, the output stream is always perfectly smooth — one packet per tick, never more. This is fundamentally different from queuing without rate control, where a downstream receiver would see the same spikes the sender produced. The price is that excess packets are dropped, not delayed.
Comments
Loading comments...