cake-autorate for macOS.
Latency-driven adaptive bandwidth shaping on Apple Silicon, using only sudo.
Here's what this tool measured on a Starlink link, idle and then saturated, comparing like with like at each percentile:
idle under 278 Mbit/s of download
median 27.5 ms 66.2 ms +38.7
p90 89.1 ms 144.6 ms +55.5
gateway 3.2 ms throughout, so the queue is at the ISP and not on your LAN
Every SSH keystroke, every Zoom frame, every DNS lookup waits behind bulk download parked in somebody else's buffer. Paying for more bandwidth doesn't help; it gives the buffer more to fill.
An earlier version of this README claimed +345 ms here. That was a loaded p90 measured against an idle minimum, which counts the link's own idle jitter as bufferbloat and overstates it about fourfold on satellite. The numbers above are the honest ones.
The fix has been known since 2011: send slightly slower than the link can carry, so the queue forms where you can manage it. Linux does this beautifully with CAKE. The awkward part is picking the rate, because on satellite and cellular links capacity moves constantly. During one afternoon of testing, this link ranged between 135 and 392 Mbit/s.
That's the problem cake-autorate solves: watch latency, steer the shaper in real time. MaCake ports that controller to macOS.
Drift-controlled, unshaped runs on either side so link variance can't be mistaken for a result:
| throughput | added median | added p90 | |
|---|---|---|---|
| off | 278.52 Mbit/s | +38.7 ms | +55.5 ms |
| on | 177.51 Mbit/s | +9.8 ms | +16.0 ms |
| off again | 290.78 Mbit/s | +49.4 ms | +64.5 ms |
Four times less added delay, for a bit over a third of the throughput. No version of this is free: a pipe only owns the queue while it is the bottleneck, so it has to sit under your line rate. That is the whole trade, and it's the same one CAKE makes on Linux.
Default mode shapes permanently, starting at 95% of measured capacity and steering from
there — up to but never above what the link has been measured carrying, down whenever a
queue shows. That is upstream cake-autorate's own posture. --mode balanced does the
opposite and stays out of the datapath until a queue actually stands, then releases after
twenty seconds clean:
[14:36] standing queue for 4s (delay +18.5 ms one-way) - engaging shaper
[14:37] latency clean for 20s — releasing shaper
Balanced exists because six drift-controlled runs on this Starlink link found no standing queue worth removing, and a pipe that isn't needed costs throughput for nothing. If your link bloats consistently, which most cable and DSL lines do, leave it on the default. All of it, including the runs where shaping lost, is in docs/MEASUREMENTS.md.
No, and the difference matters enough to put in the second section rather than bury it.
CAKE is a Linux kernel qdisc. XNU has no qdisc subsystem, so there is nothing for
sch_cake to plug into, and getting one would need a kernel extension. What MaCake ports
is cake-autorate: the controller that sits above the shaper and decides what rate it
should run at. That algorithm doesn't care what's underneath it, so it drives macOS
dummynet pipes through pf instead of Linux tc.
| cake-autorate's control algorithm | Ported. Asymmetric baseline EWMA (α↓ 0.9, α↑ 0.005), sliding bufferbloat window, idle/low/high × bufferbloat load classes, get_next_shaper_rate() with upstream's proportional adjustments (cut 0.99→0.75, raise 1.00→1.04, decay 0.99/1.01), 300 ms and 1000 ms refractory periods, two independent per-direction loops |
| Rate shaping, BDP-sized queues, ingress and egress | Yes, via dnctl pipes |
| fq_codel or COBALT AQM inside the shaper | No. macOS dummynet has droptail and RED, nothing else |
| Per-flow fair queueing (CAKE's DRR++) | No, and it isn't a configuration problem. Three separate proofs |
| Host fairness, diffserv tins, ACK filtering | No |
If you control your router, put OpenWrt and real CAKE on it and run upstream cake-autorate there; it will beat this. MaCake exists for the cases where you can't: a Starlink terminal you're not allowed to replace, a phone hotspot, hotel wifi, a laptop that moves between all of them in one day.
macOS already ships FQ-CoDel. Run netstat -qq -I en0 and you'll see:
en0: [ sched: FQ_CODEL qlength: 0/6720 ]
[ pri: VO (1) quantum: 605 drr_max: 8 ]
[ pri: VI (2) quantum: 3028 drr_max: 6 ]
[ pri: BE (7) quantum: 1514 drr_max: 4 ]
[ target qdelay: 10.00 msec update interval: 100.00 msec ]
[ L4S target qdelay: 15.00 msec ]
[ flows total: 0 new: 0 old: 0 ]
Per-flow queues, deficit round robin, priority classes, a CoDel target, L4S. Apple built most of CAKE's ideas into the interface queue and nobody talks about it.
It doesn't rescue you, for two reasons. It only covers egress, so download bloat is untouched, and on this Starlink link download was the far worse direction. It also only engages when the local NIC is the bottleneck, which on satellite or cellular it never is. Details in docs/DUMMYNET.md.
git clone https://github.com/dlukel/MaCake.git
cd MaCake
./install.shYou need the Xcode Command Line Tools (xcode-select --install) and nothing else. No
Xcode, no Apple Developer account, no entitlement, no SIP change, no reduced-security boot.
cakectl doctor # what does this Mac actually support? prints what's missing too
cakectl measure # capacity, idle baseline, bufferbloat under load, saved as a profile
sudo cakectl up # install the shaper, sized from that measurement
sudo cakectl autorate # run the control loop
sudo cakectl down # remove everything; idempotent, safe after a crashTo keep it running:
sudo cakectl install-daemonThere's also a menu bar app in dist/MaCake.app. It shows the controller's live state and
toggles the daemon; every privileged action goes through cakectl and macOS's own
authorisation prompt, so the app never handles your password.
| command | root | |
|---|---|---|
measure |
Detect the link, sample idle RTT, saturate both directions, cross-check against networkQuality, write a profile |
|
status |
Current link, live controller state, saved profiles | |
monitor |
Continuous RTT sampling and rolling baseline; no shaping | |
bench |
One-shot throughput and latency-under-load, for A/B testing a change | |
doctor |
Capability probe | |
links |
Every saved link profile | |
up / down |
✓ | Install and remove the shaper |
autorate |
✓ | The control loop |
install-daemon |
✓ | Run autorate at boot |
install-agent |
Run monitor at login, no root needed |
┌──────────────────────────────────────────┐
ICMP echo ────►│ RTTSampler │
1.1.1.1 │ per-reflector baseline │
8.8.8.8 │ α↓0.9 / α↑0.005 + 300 s window min │
9.9.9.9 │ │ │
│ ▼ │
getifaddrs ───►│ DirectionController × 2 (dl, ul) │
achieved rate │ load = achieved / shaper │
│ bufferbloat = N of last M over thresh │
│ get_next_shaper_rate() │
└────────────────────┬─────────────────────┘
▼
dnctl pipe <n> config bw <k>Kbit/s queue <q>Kbytes
│
┌───────────────────────────────────┴──────────────────────┐
│ main ruleset: dummynet-anchor "cakectl" │
│ anchor cakectl: dummynet out on en0 all pipe 4801 │
│ dummynet in on en0 all pipe 4802 │
└──────────────────────────────────────────────────────────┘
Two latency baselines get tracked, because they answer different questions. A hard minimum over a long rolling window (300 s by default) is what queue sizing uses. An asymmetric EWMA that falls fast toward a new minimum and rises very slowly is what the controller steers on, matching upstream. The gateway gets probed too, but only for diagnosis: on satellite and cellular the bottleneck sits well beyond it, so using it as the control signal would be useless.
Queue depth comes from the bandwidth-delay product at the measured baseline, floored at 4 MTU and capped at 1023 KB, because dnctl refuses a byte-denominated queue of 1 MB or more. Never a fixed packet count.
On pf, MaCake reconstructs your live main ruleset from pfctl output, adds exactly one
line, and keeps its own rules in a dedicated anchor. Teardown rebuilds from whatever is
live at that moment rather than restoring a stale snapshot, so anchors another service
added meanwhile survive. pfctl -E/-X reference counting means cakectl down won't
switch pf off underneath something else that turned it on. If your main ruleset contains
anything MaCake doesn't recognise, it refuses to touch it and tells you the one line to
add by hand.
Profiles live in ~/.config/cakectl/profiles.json, keyed by SSID where macOS discloses it,
otherwise interface plus gateway MAC, otherwise interface plus gateway IP. Alias keys get
stored alongside, because a profile written by your user account has to be findable by the
root daemon, and root sees facts your account can't. On a link change the controller
reloads that link's profile and starts from its known-good rate rather than probing up
from zero.
- docs/DUMMYNET.md: every pf and dummynet behaviour this depends on, verified by measurement, including eight that are undocumented or actively misleading
- docs/WHAT-THIS-IS-NOT.md: the honest scope
- docs/MEASUREMENTS.md: reproducible numbers, including the two controller bugs a real link shook out
- CONTRIBUTING.md
The rate-control algorithm comes from cake-autorate by lynxthecat and contributors, GPL-2.0. No source is copied here; the published algorithm is reimplemented in Swift against a different shaper. Star that project too, it's the original idea and it's still the better option if you own your router.
CAKE and the bufferbloat work behind all of this belong to Dave Täht, Toke Høiland-Jørgensen, Jonathan Morton, and everyone at Bufferbloat.net.
Apache License 2.0. Copyright 2026 Loucas Louka. See LICENSE and NOTICE.
Use it, ship it, sell it. Keep the copyright notice and the NOTICE file.