The simulation below models a single bottleneck link. You control the link's capacity and the propagation delay. Run BBR to see how it probes, estimates, and then locks into the right sending rate. Run Loss-Based to compare the classic approach.
<!-- BBR vs Loss-Based Congestion Control simulation -->
<div class="controls">
<label>Bottleneck BW: <input type="range" id="bw" min="1" max="20" value="10"> <span id="bw-val">10</span> Mbps</label>
<label>RTT: <input type="range" id="rtt" min="10" max="200" value="50"> <span id="rtt-val">50</span> ms</label>
</div>
<div class="btn-row">
<button id="btn-bbr" type="button">Run BBR</button>
<button id="btn-loss" type="button" class="secondary">Run Loss-Based</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="chart" width="560" height="220"></canvas>
<div class="legend">
<span class="dot bbr"></span> BBR
<span class="dot loss"></span> Loss-based
<span class="dot bdp"></span> BDP
</div>
<div id="info" class="info">Adjust the link parameters, then run a sender.</div>
/* Demo styles — BBR congestion control */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin-bottom: .6rem; font-size: .85rem; }
label { display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 110px; cursor: pointer; }
.btn-row { display: flex; gap: .5rem; margin-bottom: .7rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer; border: 1px solid #1d4e7a; }
button:not(.secondary):not(.ghost) { background: #1d4e7a; color: #fff; }
button.secondary { background: #c0392b; color: #fff; border-color: #c0392b; }
button.ghost { background: #fff; color: #333; border-color: #aaa; }
canvas { display: block; max-width: 100%; border: 1px solid #dde3ea; border-radius: 8px; background: #fafcfd; }
.legend { font-size: .78rem; margin-top: .35rem; display: flex; align-items: center; gap: .25rem; color: #555; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.bbr { background: #1d4e7a; }
.dot.loss { background: #c0392b; }
.dot.bdp { background: #2ecc71; border: 2px solid #27ae60; background: transparent; border-radius: 0; height: 2px; width: 18px; }
.info { font-size: .85rem; margin-top: .45rem; min-height: 1.5em; color: #444; }
.info.ok { color: #0a7d33; font-weight: 600; }
.info.warn { color: #b7700a; font-weight: 600; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// BBR and loss-based congestion control simulation
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 18, right: 20, bottom: 30, left: 52 };
const PLOT_W = W - PAD.left - PAD.right;
const PLOT_H = H - PAD.top - PAD.bottom;
// Simulation parameters
const ROUNDS = 40; // number of RTT rounds to simulate
let bwMbps = 10;
let rttMs = 50;
document.getElementById('bw').addEventListener('input', function() {
bwMbps = +this.value;
document.getElementById('bw-val').textContent = bwMbps;
});
document.getElementById('rtt').addEventListener('input', function() {
rttMs = +this.value;
document.getElementById('rtt-val').textContent = rttMs;
});
// BBR simulation — startup, drain, ProbeBW phases
// All rates in Mbps; bufferPct is fraction of capacity overshoot
function simulateBBR() {
const cap = bwMbps; // link capacity in Mbps
const data = [];
let sendRate = cap * 0.05; // start at 5% of capacity
let phase = 'startup'; // start in startup phase
let drainRounds = 0;
for (let round = 0; round < ROUNDS; round++) {
let tput, bufferPct;
if (phase === 'startup') {
// Startup — double send rate each round until near capacity
sendRate = Math.min(sendRate * 2, cap * 1.1);
tput = Math.min(sendRate, cap);
bufferPct = Math.max(0, (sendRate - cap) / cap);
if (tput >= cap * 0.95) { phase = 'drain'; drainRounds = 0; }
} else if (phase === 'drain') {
// Drain — reduce rate to clear the buffer overfill
drainRounds++;
sendRate = Math.max(sendRate * 0.6, cap * 0.9);
tput = Math.min(sendRate, cap);
bufferPct = Math.max(0, (sendRate - cap) / cap) * 0.3;
if (drainRounds >= 3) { phase = 'probebw'; sendRate = cap; }
} else {
// ProbeBW — steady state, probe 5% higher every 8 rounds
const probeGain = (round % 8 === 0) ? 1.05 : 1.0;
sendRate = cap * probeGain;
tput = Math.min(sendRate, cap) * 0.98;
bufferPct = Math.max(0, (sendRate - cap) / cap) * 0.1;
}
data.push({ round, tput: tput.toFixed(3), bufferPct: bufferPct.toFixed(4), phase });
}
return data;
}
// Loss-based (classic TCP CUBIC-like) simulation
function simulateLoss() {
const cap = bwMbps;
const data = [];
let sendRate = cap * 0.05; // effective send rate starts small
let ssthresh = cap * 2;
for (let round = 0; round < ROUNDS; round++) {
let tput, bufferPct;
if (sendRate < ssthresh) {
// Slow start — double send rate
sendRate = Math.min(sendRate * 2, ssthresh);
} else {
// Congestion avoidance — additive increase
sendRate += cap * 0.08;
}
bufferPct = Math.max(0, (sendRate - cap) / cap);
tput = Math.min(sendRate / (1 + bufferPct * 0.5), cap);
// Buffer overflow triggers multiplicative decrease
if (bufferPct > 0.5 && Math.random() < 0.6) {
ssthresh = sendRate * 0.5;
sendRate = ssthresh;
bufferPct *= 0.5;
tput *= 0.6;
}
data.push({ round, tput: tput.toFixed(3), bufferPct: bufferPct.toFixed(4) });
}
return data;
}
let bbrData = null;
let lossData = null;
function drawChart() {
ctx.clearRect(0, 0, W, H);
const cap = bwMbps;
// Draw axes
ctx.strokeStyle = '#ccd4db';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + PLOT_H);
ctx.lineTo(PAD.left + PLOT_W, PAD.top + PLOT_H);
ctx.stroke();
// Y-axis tick marks
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const v = (cap * i / 4).toFixed(1);
const y = PAD.top + PLOT_H - (i / 4) * PLOT_H;
ctx.fillText(v, PAD.left - 5, y + 3);
ctx.strokeStyle = '#eaeef2';
ctx.beginPath();
ctx.moveTo(PAD.left, y);
ctx.lineTo(PAD.left + PLOT_W, y);
ctx.stroke();
}
// Y-axis label
ctx.save();
ctx.translate(12, PAD.top + PLOT_H / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillStyle = '#666';
ctx.font = '10px system-ui';
ctx.fillText('Throughput (Mbps)', 0, 0);
ctx.restore();
// X-axis label
ctx.fillStyle = '#888';
ctx.textAlign = 'center';
ctx.font = '10px system-ui';
ctx.fillText('Round-trip rounds', PAD.left + PLOT_W / 2, H - 4);
// Capacity reference line (100% = BDP filled)
const bdpLineY = PAD.top + PLOT_H - PLOT_H; // green dashed line at the top = full capacity
ctx.setLineDash([5, 4]);
ctx.strokeStyle = '#2ecc71';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left + PLOT_W, PAD.top);
ctx.stroke();
ctx.setLineDash([]);
function drawLine(data, color) {
if (!data) return;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
data.forEach((d, i) => {
const x = PAD.left + (i / (ROUNDS - 1)) * PLOT_W;
const y = PAD.top + PLOT_H - Math.min(+d.tput / cap, 1) * PLOT_H;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
}
drawLine(lossData, '#c0392b');
drawLine(bbrData, '#1d4e7a');
}
function setInfo(msg, cls) {
const el = document.getElementById('info');
el.textContent = msg;
el.className = 'info ' + (cls || '');
}
document.getElementById('btn-bbr').addEventListener('click', function() {
bbrData = simulateBBR();
drawChart();
const finalTput = +bbrData[bbrData.length - 1].tput;
const eff = ((finalTput / bwMbps) * 100).toFixed(0);
setInfo(i18nFmt('BBR reached %eff%% of capacity — pipe full, buffer near empty.', { eff: eff }), 'ok');
});
document.getElementById('btn-loss').addEventListener('click', function() {
lossData = simulateLoss();
drawChart();
const finalTput = +lossData[lossData.length - 1].tput;
const eff = ((finalTput / bwMbps) * 100).toFixed(0);
setInfo(i18nFmt('Loss-based reached %eff%% of capacity — buffer may be inflated.', { eff: eff }), 'warn');
});
document.getElementById('btn-reset').addEventListener('click', function() {
bbrData = null;
lossData = null;
drawChart();
setInfo('Adjust the link parameters, then run a sender.', '');
});
drawChart();
<!-- BBR vs Loss-Based Congestion Control simulation -->
<div class="controls">
<label>Bottleneck BW: <input type="range" id="bw" min="1" max="20" value="10"> <span id="bw-val">10</span> Mbps</label>
<label>RTT: <input type="range" id="rtt" min="10" max="200" value="50"> <span id="rtt-val">50</span> ms</label>
</div>
<div class="btn-row">
<button id="btn-bbr" type="button">Run BBR</button>
<button id="btn-loss" type="button" class="secondary">Run Loss-Based</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="chart" width="560" height="220"></canvas>
<div class="legend">
<span class="dot bbr"></span> BBR
<span class="dot loss"></span> Loss-based
<span class="dot bdp"></span> BDP
</div>
<div id="info" class="info">Adjust the link parameters, then run a sender.</div>
/* Demo styles — BBR congestion control */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin-bottom: .6rem; font-size: .85rem; }
label { display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 110px; cursor: pointer; }
.btn-row { display: flex; gap: .5rem; margin-bottom: .7rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer; border: 1px solid #1d4e7a; }
button:not(.secondary):not(.ghost) { background: #1d4e7a; color: #fff; }
button.secondary { background: #c0392b; color: #fff; border-color: #c0392b; }
button.ghost { background: #fff; color: #333; border-color: #aaa; }
canvas { display: block; max-width: 100%; border: 1px solid #dde3ea; border-radius: 8px; background: #fafcfd; }
.legend { font-size: .78rem; margin-top: .35rem; display: flex; align-items: center; gap: .25rem; color: #555; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.bbr { background: #1d4e7a; }
.dot.loss { background: #c0392b; }
.dot.bdp { background: #2ecc71; border: 2px solid #27ae60; background: transparent; border-radius: 0; height: 2px; width: 18px; }
.info { font-size: .85rem; margin-top: .45rem; min-height: 1.5em; color: #444; }
.info.ok { color: #0a7d33; font-weight: 600; }
.info.warn { color: #b7700a; font-weight: 600; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// BBR and loss-based congestion control simulation
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 18, right: 20, bottom: 30, left: 52 };
const PLOT_W = W - PAD.left - PAD.right;
const PLOT_H = H - PAD.top - PAD.bottom;
// Simulation parameters
const ROUNDS = 40; // number of RTT rounds to simulate
let bwMbps = 10;
let rttMs = 50;
document.getElementById('bw').addEventListener('input', function() {
bwMbps = +this.value;
document.getElementById('bw-val').textContent = bwMbps;
});
document.getElementById('rtt').addEventListener('input', function() {
rttMs = +this.value;
document.getElementById('rtt-val').textContent = rttMs;
});
// BBR simulation — startup, drain, ProbeBW phases
// All rates in Mbps; bufferPct is fraction of capacity overshoot
function simulateBBR() {
const cap = bwMbps; // link capacity in Mbps
const data = [];
let sendRate = cap * 0.05; // start at 5% of capacity
let phase = 'startup'; // start in startup phase
let drainRounds = 0;
for (let round = 0; round < ROUNDS; round++) {
let tput, bufferPct;
if (phase === 'startup') {
// Startup — double send rate each round until near capacity
sendRate = Math.min(sendRate * 2, cap * 1.1);
tput = Math.min(sendRate, cap);
bufferPct = Math.max(0, (sendRate - cap) / cap);
if (tput >= cap * 0.95) { phase = 'drain'; drainRounds = 0; }
} else if (phase === 'drain') {
// Drain — reduce rate to clear the buffer overfill
drainRounds++;
sendRate = Math.max(sendRate * 0.6, cap * 0.9);
tput = Math.min(sendRate, cap);
bufferPct = Math.max(0, (sendRate - cap) / cap) * 0.3;
if (drainRounds >= 3) { phase = 'probebw'; sendRate = cap; }
} else {
// ProbeBW — steady state, probe 5% higher every 8 rounds
const probeGain = (round % 8 === 0) ? 1.05 : 1.0;
sendRate = cap * probeGain;
tput = Math.min(sendRate, cap) * 0.98;
bufferPct = Math.max(0, (sendRate - cap) / cap) * 0.1;
}
data.push({ round, tput: tput.toFixed(3), bufferPct: bufferPct.toFixed(4), phase });
}
return data;
}
// Loss-based (classic TCP CUBIC-like) simulation
function simulateLoss() {
const cap = bwMbps;
const data = [];
let sendRate = cap * 0.05; // effective send rate starts small
let ssthresh = cap * 2;
for (let round = 0; round < ROUNDS; round++) {
let tput, bufferPct;
if (sendRate < ssthresh) {
// Slow start — double send rate
sendRate = Math.min(sendRate * 2, ssthresh);
} else {
// Congestion avoidance — additive increase
sendRate += cap * 0.08;
}
bufferPct = Math.max(0, (sendRate - cap) / cap);
tput = Math.min(sendRate / (1 + bufferPct * 0.5), cap);
// Buffer overflow triggers multiplicative decrease
if (bufferPct > 0.5 && Math.random() < 0.6) {
ssthresh = sendRate * 0.5;
sendRate = ssthresh;
bufferPct *= 0.5;
tput *= 0.6;
}
data.push({ round, tput: tput.toFixed(3), bufferPct: bufferPct.toFixed(4) });
}
return data;
}
let bbrData = null;
let lossData = null;
function drawChart() {
ctx.clearRect(0, 0, W, H);
const cap = bwMbps;
// Draw axes
ctx.strokeStyle = '#ccd4db';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + PLOT_H);
ctx.lineTo(PAD.left + PLOT_W, PAD.top + PLOT_H);
ctx.stroke();
// Y-axis tick marks
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const v = (cap * i / 4).toFixed(1);
const y = PAD.top + PLOT_H - (i / 4) * PLOT_H;
ctx.fillText(v, PAD.left - 5, y + 3);
ctx.strokeStyle = '#eaeef2';
ctx.beginPath();
ctx.moveTo(PAD.left, y);
ctx.lineTo(PAD.left + PLOT_W, y);
ctx.stroke();
}
// Y-axis label
ctx.save();
ctx.translate(12, PAD.top + PLOT_H / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillStyle = '#666';
ctx.font = '10px system-ui';
ctx.fillText('Throughput (Mbps)', 0, 0);
ctx.restore();
// X-axis label
ctx.fillStyle = '#888';
ctx.textAlign = 'center';
ctx.font = '10px system-ui';
ctx.fillText('Round-trip rounds', PAD.left + PLOT_W / 2, H - 4);
// Capacity reference line (100% = BDP filled)
const bdpLineY = PAD.top + PLOT_H - PLOT_H; // green dashed line at the top = full capacity
ctx.setLineDash([5, 4]);
ctx.strokeStyle = '#2ecc71';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left + PLOT_W, PAD.top);
ctx.stroke();
ctx.setLineDash([]);
function drawLine(data, color) {
if (!data) return;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
data.forEach((d, i) => {
const x = PAD.left + (i / (ROUNDS - 1)) * PLOT_W;
const y = PAD.top + PLOT_H - Math.min(+d.tput / cap, 1) * PLOT_H;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
}
drawLine(lossData, '#c0392b');
drawLine(bbrData, '#1d4e7a');
}
function setInfo(msg, cls) {
const el = document.getElementById('info');
el.textContent = msg;
el.className = 'info ' + (cls || '');
}
document.getElementById('btn-bbr').addEventListener('click', function() {
bbrData = simulateBBR();
drawChart();
const finalTput = +bbrData[bbrData.length - 1].tput;
const eff = ((finalTput / bwMbps) * 100).toFixed(0);
setInfo(i18nFmt('BBR reached %eff%% of capacity — pipe full, buffer near empty.', { eff: eff }), 'ok');
});
document.getElementById('btn-loss').addEventListener('click', function() {
lossData = simulateLoss();
drawChart();
const finalTput = +lossData[lossData.length - 1].tput;
const eff = ((finalTput / bwMbps) * 100).toFixed(0);
setInfo(i18nFmt('Loss-based reached %eff%% of capacity — buffer may be inflated.', { eff: eff }), 'warn');
});
document.getElementById('btn-reset').addEventListener('click', function() {
bbrData = null;
lossData = null;
drawChart();
setInfo('Adjust the link parameters, then run a sender.', '');
});
drawChart();
Notice how BBR reaches full throughput quickly and keeps the in-flight data near the bandwidth-delay product (BDP) — the ideal pipe fill. The loss-based sender overshoots, fills the buffer, and then oscillates. The difference is not raw speed but where in the system the data piles up.
Comments
Loading comments...