Per-application bandwidth limits and traffic prioritisation for Linux.
Install · How to use · Testing · Releasing
Watch who uses your connection — then decide who wins. Throtl shows every
app's live download/upload and lets you limit, prioritise or schedule it: per
app, per direction, per weekday/time window. Rolling budgets alert you — and can
throttle you to a floor — before you hit your data cap. A native dashboard
(Tauri 2 + React) drives a root daemon that does the real tc/cgroup shaping
(TrafficToll) and
nethogs measurement.
git clone https://github.com/Flenser42/throtl.git && cd throtl && sudo ./setup/install.shOne command — Arch · Debian/Ubuntu · Fedora · openSUSE (auto-detects your package manager). → Full installation guide · prebuilt packages · AUR
- Monitor — a live graph and per-app rates: see exactly who's using the connection.
- Control — per-app download/upload limits and Critical→Low priorities, plus a global cap.
- Schedule — rules that apply only in a weekday/time window; profiles switch automatically.
- Budget — rolling day/week volume caps with alerts and optional enforcement (throttle to a floor).
- Automate — startup profile, background autostart and a live bar/tray widget (Omarchy/Waybar).
The dashboard — more screenshots in the usage guide.
Throtl is one loop: watch who uses the connection, then decide who wins. The features split into monitoring, control, budgets and automation.
- Live graph — download/upload over time on a real time axis. Shows the last 60 s by default and auto-scrolls; switch the window to 30 s / 1 min / 5 min / 15 min / All and scroll back through history at any time.
- Live process table — per-app rates, editable limits and priority, sortable columns, colour-coded up/down values, and a filter box to focus on one app.
- Explains itself — every affected row says which rule applies (own limit, global limit, time window and whether it is active right now).
- Per-application limits — download/upload caps for a single app, even when it runs many processes (they are grouped into one row and summed).
- Priorities — Critical / High / Normal / Low for individual apps, plus a global default priority for everything that has no rule.
- Time windows — give any rule a weekday + time window ("Firefox, Mon–Fri 20:00–00:00"); the rule only applies inside it. Windows across midnight work, and the engine re-applies automatically when a window opens or closes.
- Global switch — turn all shaping on/off without losing your rules.
- Interface switching — follow the default route automatically (WLAN ↔ LAN, VPN up/down) or pin shaping to a specific interface from Settings → Interface.
- Consumption budgets — a rolling daily/weekly volume limit (global or per app); the GUI warns and shows a desktop notification when it is exceeded.
- Editable budgets — the Budgets sheet sets the global daily/weekly volume and per-application budgets, showing how much is already used. A desktop notification arrives at 80 % of a budget, so there is still time to react.
- Statistics — persistent per-app history over 1 h / 2 days / 30 days, shown as a table and a graph.
- Budget enforcement — optionally throttle an over-budget app (or the whole
connection, for a global budget) to a configurable floor rate, and revert when
the rolling window moves on. Alerts live in the daemon, so any client can react.
See Budgets for the
enforce/floorconfig.
- Profiles, schedules & startup profile — save the current limits as named profiles ("University", "Evening", "Night"), switch between them automatically by weekday and time, and pick one to activate on daemon startup.
- Native-feeling design — follows your system light/dark preference (or pin Light/Dark in Settings); the graph and rate colours adapt with it. A guided tour introduces the layout on first launch.
- Responsive — TrafficToll restarts are coalesced and happen off the UI thread, so the window never freezes while a change is applied.
- Headless CLI — everything the GUI can do, plus
monitor,top,watch(a timed report with an optional alert threshold) and a simulation mode that needs neither root nor TrafficToll. - Local only — a Unix socket, no network port. The one outbound request is the optional update check (below).
- Knows when the service is missing — if the daemon is not reachable, the window shows a status page with Try again and Setup guide instead of an empty table.
- Update notice — tells you when a newer release exists and opens its page in your browser. Throtl never installs anything by itself.
- Start in background — launch on login and run minimised to the tray (Settings → Startup); offered by the first-run wizard too.
- System-tray widget — live download/upload and a click-to-toggle shaping switch, for the Omarchy bar or Waybar (see Widgets).
- Command palette —
Ctrl/⌘ Popens a searchable, keyboard-driven list of actions (focus the filter, add a rule, open a sheet, switch profile, toggle theme or shaping, …).
- The privileged daemon (
throtl-daemon) runs asrootvia systemd (throtl.service) becausetc, the IFB device andnethogsneed root. - The IPC is a local Unix socket
(
/run/throtl/daemon.sock) carrying newline-delimited JSON. No TCP port is opened. - Configuration is persisted as TOML:
/etc/throtl/config.tomlunder systemd, or~/.config/throtl/config.tomlfor manual runs. The GUI reads it only through the daemon API, never from the file.
TrafficToll reads its YAML config once at startup and installs the tc filters
for the matched processes; it has no SIGHUP reload. Throtl therefore writes the
current config and restarts the tt subprocess on every change:
change (GUI/CLI) ──► render YAML ──► stop tt ──► start tt
This is the most reliable path and does not require restarting the target
application — the tc rules apply immediately on the interface.
Priority numbers map as Critical 0 … Low 3 (lower = served first). The
global priority is the class used for traffic that matches no per-app rule.
Prioritisation only has a visible effect when the link is saturated and a
global download/upload cap is set — otherwise TrafficToll runs at line rate
and there is no queue to prioritise. The GUI hint states this as well.
Throtl sets download and upload priority together from a single control.
Throtl runs nethogs in trace mode with -v 1 (cumulative kB per process)
and computes the rate itself from the delta between two ticks over a monotonic
clock. nethogs' built-in rate mode divides by its assumed PERIOD and drifts
badly under load (we measured 2.0 MB/s reported while the kernel and the
application showed ~2.5 MB/s for the same traffic), so the deltas are used
directly. nethogs counts KiB (1024-byte units), which Throtl converts to its
internal kbit/s schema. Traffic that cannot be attributed to a process is kept
as a synthetic (unattributed) row instead of being dropped.
- Linux — Arch / Manjaro / Omarchy, Debian / Ubuntu, Fedora or openSUSE
(anything with
systemdand a package manager). Developed on Omarchy (Wayland). - Python 3.11+
traffictoll— installed by the setup script into a venv under/opt/throtltcand theifbkernel module (for ingress shaping)nethogsand the WebKitGTK 4.1 runtime for the dashboard —install.shinstalls these for you; the exact package names per distro are in INSTALL.md.- Building the dashboard from source additionally needs Node 20+ and Rust (see INSTALL.md); the CLI needs neither.
The backend itself has no third-party Python dependencies — it uses only the standard library.
One command works on Arch / Omarchy, Debian / Ubuntu, Fedora and openSUSE (the script detects your package manager):
git clone https://github.com/Flenser42/throtl.git
cd throtl
sudo ./setup/install.shBeginner-friendly guide — per-distro system packages, prebuilt packages, the dashboard build, and troubleshooting: INSTALL.md.
The script:
-
installs the system packages it needs (
nethogs,webkit2gtk-4.1,iproute2fortc), -
creates
/opt/throtl/venvand installstraffictoll, -
copies the code and launchers to
/opt/throtl(and symlinksthrotl-cli/throtl-daemoninto/usr/local/bin), -
creates
/etc/throtl/config.tomland/run/throtl, -
installs and starts the
throtlsystemd service, -
removes leftovers from the old GTK GUI (menu entry, launcher, icon),
-
installs the dashboard GUI for your user (builds the AppImage once if needed, then installs it under
~/.local). Use--no-appto skip it, or run it separately later.The first GUI build compiles Rust and can take 5–15 minutes. The installer shows a live progress bar — a percentage driven by the number of compiled crates, with phase labels (deps → frontend → Rust → bundling) — so it is working, not stuck; don't interrupt it. Later installs reuse the cached build and finish in seconds, but the cache is freshness-checked: if any source file is newer than the built AppImage (e.g. after
git pull), the dashboard is rebuilt automatically.
That is the whole install — daemon + GUI in one command:
sudo ./setup/install.sh # everything (asks about the bar widget)
sudo ./setup/install.sh --no-app # daemon + CLI only
sudo ./setup/install.sh --with-omarchy # + the Omarchy bar widget
sudo ./setup/install.sh --with-waybar # + the Waybar status module
sudo ./setup/install.sh --with-widget # + whichever matches your shell
./setup/install-app.sh # dashboard only (no root)
make install # same as the first lineThe installer detects whether you run Omarchy (omarchy-shell) or a generic
Arch setup, and asks only about the matching bar widget (or you can force one
with the flags above). If the dashboard is already running, the installer closes
it before reinstalling and relaunches it afterwards.
Once installed, the dashboard can grant your account access again without a
terminal: if it shows "No access to the daemon socket", click Grant
access — this runs the setup helper through polkit (pkexec) and asks for
your admin password, then adds you to the throtl group (the socket stays
root:throtl 0660). Log out and back in afterwards. The equivalent from a
source checkout is pkexec ./setup/throtl-setup. The very first install still
starts at the terminal (or a distro package), because the polkit helper and
policy must already be on the machine.
Uninstall with sudo ./setup/uninstall.sh (add --purge to also remove code
and configuration), which also removes the per-user dashboard; to drop only the
dashboard by hand:
rm -rf ~/.local/opt/throtl ~/.local/bin/throtl-app \
~/.local/share/applications/throtl-app.desktop \
~/.local/share/icons/hicolor/scalable/apps/throtl-app.svgPrebuilt artifacts / distro packages: The backend is pure Python, so there is nothing to compile for the daemon or CLI. Every release carries an sdist, a wheel, a Debian
.debfor the backend (build it locally withmake deb), and a Tauri.deb/.rpmfor the dashboard (make app-build); there is an AURPKGBUILDfor Arch/Omarchy. How to install these by hand: INSTALL.md. Seepackaging/README.md.CI lints and tests the Python backend (ruff + unittest), builds the frontend (
npm run build) and denies Rust warnings (cargo clippy -D warnings).
The daemon and engine need root, so Throtl installs system-wide. The frontend (GUI/CLI) runs as your user and talks to the daemon over the Unix socket.
Open Throtl from your launcher, or:
throtl-appThe window is one monitor surface — the live download/upload rates, the bandwidth graph and a compact status line (active rules, global caps, profile) — above the application list:
- Graph — hover for a crosshair and a tooltip with the exact values at that
moment, click to pin it; switch the window with
30s / 1m / 5m / 15m / all. Below it:min / avg / maxand the peak application. - Applications — sortable and filterable. Each row shows the rates, a per-app
arm switch and a
⋯menu (edit rule, reset counters, copy the equivalent CLI command, remove rule). The second line keeps only what applies: limit (◧ 3.4 MB/s ↓ · 0.5 ↑orunlimited), priority (icon + word), time window (with· nowwhen active) and a budget bar. - Theme — follows the system; press
Lto toggle light/dark. - Filter —
Ctrl/⌘ Kfocuses the search box. - Command palette —
Ctrl/⌘ Popens a searchable list of actions (add a rule, open a sheet, switch profile, toggle theme or shaping, …); arrows + Enter run,Esccloses.
Colours, spacing and type come from the design tokens in
throtl-app/src/styles/globals.css; the
Rust side never exposes the socket to the webview.
Limits are displayed and interpreted in the selected unit (MB/s, Mbit/s, KB/s, kbit/s). An explicit suffix such as
2 kbpsalways wins over the unit.
| Shortcut | Action |
|---|---|
L |
Toggle light/dark |
Ctrl/⌘ K |
Focus the application filter |
Ctrl/⌘ P |
Open the command palette |
Esc |
Close the open sheet or menu |
On start, Throtl asks the public GitHub release API which version is current and shows a banner when yours is older — “Throtl 0.12.0 is available — you have 0.11.0” — with a View release button that opens the release page in your browser. Throtl never downloads or installs anything, needs no root for this and asks nothing of the daemon.
This is the only outbound request the app makes. It carries no account, no identifier and no usage data — just a GET for the latest tag. Switch it off in Settings under Updates; Check now runs it once on demand.
The daemon is fully controllable without the dashboard:
throtl-cli status
throtl-cli list-processes
# Global caps (2 Mbit/s down, 1 Mbit/s up)
throtl-cli set-global --download-limit 2mbps --upload-limit 1mbps
throtl-cli set-global --download-priority hoch
# Rule for Firefox
throtl-cli set-process --name Firefox --exe /usr/lib/firefox/firefox \
--download-limit 2mbps --priority hoch
# Only throttle Firefox on weekdays between 20:00 and midnight
throtl-cli set-process --name Firefox --exe /usr/lib/firefox/firefox \
--download-limit 1mbps --window-days mo-fr \
--window-start 20:00 --window-end 00:00
throtl-cli remove-process --key 'exe:/usr/lib/firefox/firefox'
throtl-cli toggle --enabled false # pause all shaping
throtl-cli monitor # live rates, once per second
throtl-cli top # full-screen live ranking (htop-style)
# Timed report for scripting; exit code 4 if an app exceeds the threshold
throtl-cli watch --duration 30 --alert 5mbps
throtl-cli watch --duration 10 --app firefox --json
# End-to-end check that limits actually throttle (needs root + TrafficToll)
throtl-cli selftest --limit 2mbps
# Temporarily boost an app to Critical priority, then revert it automatically
throtl-cli boost --name Firefox --seconds 300
# Machine-readable output (for scripting or the bar widget)
throtl-cli status --json
throtl-cli list-processes --json
# Profiles
throtl-cli profiles # list profiles (+ active)
throtl-cli profile-save University # save current settings as "University"
throtl-cli profile-use University # activate "University"
throtl-cli profile-delete University
# Activate a profile automatically when the daemon starts
throtl-cli start-profile University
throtl-cli start-profile # show the current startup profile
throtl-cli start-profile --clear
# Statistics: last hour, last two days or last 30 days
throtl-cli stats --window minute
throtl-cli stats --window day
# Consumption budgets (rolling last 24 h / last 7 days)
throtl-cli budget-set --day 20gb --week 100gb
throtl-cli budget-set --app firefox --day 5gb
throtl-cli budgets
throtl-cli budget-remove --app firefox
# Export/import the whole configuration, and diagnose the host
throtl-cli export --output throtl.toml
throtl-cli import throtl.toml
throtl-cli doctorA profile is a named snapshot of the global limits and all process rules.
The top-level global/processes are always the active view (v0.1.0
compatibility), so the GUI and CLI keep working unchanged. Profiles live
additively next to them in config.toml:
active_profile = "Standard"
[profiles.University]
global_download_limit = 2048
global_upload_limit = 512
global_priority = "hoch"
[[profiles.University.processes]]
name = "Spotify"
match_type = "exe"
match_value = "spotify"
download_limit = 512
priority = "niedrig"
[[schedule]]
profile = "University"
days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
start = "08:00"
end = "14:00"The daemon checks the schedule once per monitoring tick and activates the
first matching profile automatically (only when schedule is non-empty).
days accepts mo…so (Mo = 0), ranges like "mo-fr", and English names;
overnight rules (end < start) run until the next morning.
A startup profile is the fallback for when no schedule matches:
start_profile = "University"It is applied once when the daemon starts; a schedule that matches at that moment still takes priority.
Individual rules can be limited to a weekday + time window. Add a window to
a rule in config.toml (flat form shown here):
[[processes]]
name = "Firefox"
match_type = "exe"
match_value = "/usr/lib/firefox/firefox"
download_limit = 1024
priority = "normal"
window_days = ["Mon", "Tue", "Wed", "Thu", "Fri"]
window_start = "20:00"
window_end = "00:00" # before start -> runs across midnightInside the window the rule is applied as usual; outside it the rule is left
out of the generated TrafficToll config, so the app is unthrottled. The daemon
re-applies automatically on the transition (once per monitoring tick). In the
GUI, the small clock button in each row opens the window editor; Clear
removes the window. The CLI accepts --window-days / --window-start /
--window-end (and --clear-window).
Budgets are rolling volume limits (day = last 24 h, week = last 7 days),
global or per app. The daemon tracks alert levels and exposes them through
get_budgets (the alerts list), so any client can react to a budget crossing
80 % or its limit. Optionally enforce them — throttle an over-budget app (or the
whole connection for a global budget) to a floor rate, and revert when the
window moves on:
[budgets]
enabled = true
enforce = true
floor = 64 # kbit/s — over-budget apps are throttled to this
[[budget_rules]]
app = "firefox"
day = "5gb"Set limits with throtl-cli budget-set, list them with throtl-cli budgets.
The daemon accumulates, per monitoring tick, the download/upload volume of
every application into three rolling windows and stores them as
<config_dir>/stats.json:
| Window | Bucket | Kept |
|---|---|---|
minute |
1 minute | 1 hour |
hour |
1 hour | 2 days |
day |
1 day | 30 days |
The dashboard exposes this in the Statistics sheet (window switcher +
per-app list and history graph); the CLI prints it with throtl-cli stats.
Stored values are bytes, derived from the sampled rates — see
docs/TESTING.md for the accuracy limits.
For a quick look without root or TrafficToll:
throtl-daemon --simulate --socket /tmp/throtl.sock &
throtl-cli --socket /tmp/throtl.sock status
throtl-cli --socket /tmp/throtl.sock set-global --download-limit 2mbpsThere are two layers:
-
Did the config reach TrafficToll?
throtl-cli statusshows preflight warnings and the engine state;systemctl status throtland the files under/etc/throtlshow the details. -
Is bandwidth actually throttled? Add a rule for your test tool and measure the real rate:
throtl-cli set-process --name curl --exe /usr/bin/curl \ --download-limit 512kbps --priority normal curl -o /dev/null -w 'Speed: %{speed_download} B/s\n' \ https://example.com/bigfile.bin
Without a real interface to shape, the simulation mode above is enough to test the control flow.
See docs/TESTING.md for manual test recipes and known
limitations.
A live bar/tray widget shows the current download/upload and a click-to-toggle shaping switch. It ships in two flavours, and the installer detects which one your system uses:
- Omarchy (
omarchy-shell) — a bar widget (flenser.throtl) with a detail popup (shaping switch, download/upload rates, global caps, top apps). Click the pill to open it, right-click to refresh. - Waybar (generic Arch) — a custom module (
throtl-waybar) showing live rates; click toggles shaping.
Install with --with-omarchy, --with-waybar or --with-widget (auto-detect);
see Installation.
- Local Unix socket only — no TCP port. The socket lives in
/run/throtl/and is owned byroot:throtlwith mode0660, so only members of thethrotlgroup (added byinstall.sh) can reach the root daemon. Access is purely local; there is no network listener. - The frontend can only set limits and priorities via the API; there is no arbitrary shell access over the socket.
- The TrafficToll YAML is rendered deterministically;
exe/namematch values are escaped,cmdlineis explicitly a regex. - The systemd unit deliberately runs without additional sandboxing because
nethogsneedscap_sys_ptrace/cap_dac_read_searchand the engine needsCAP_NET_ADMIN; the reasoning is documented inline in the unit file.
throtl/
__init__.py path/app constants
units.py kbit/s schema, rate/size parse/format
protocol.py IPC: JSON over Unix socket, client with reader thread
config.py TOML schema, priorities, rules, profiles, schedules,
time windows, rule/persistence helpers
monitor.py nethogs cumulative-mode parser + NethogsMonitor
engine.py TrafficToll YAML renderer + tt process + SimEngine
budgets.py rolling daily/weekly volume budgets
stats.py persistent per-app history (ring buffers, 3 windows)
daemon.py Unix-socket RPC daemon (root)
cli.py CLI (throtl-cli)
throtl-app/ dashboard GUI: Tauri 2 + React + TS + Tailwind v4
src/ React UI (Overview, LiveGraph, ProcessList, sheets, …)
src-tauri/ Rust shell: Unix-socket JSON-RPC bridge + commands
tests/ unittest suite (no display or root needed)
setup/ install/uninstall (+ install-app.sh) and systemd unit
packaging/ AUR PKGBUILD, Debian .deb builder + distro notes
data/ icon (SVG)
docs/ TESTING.md, RELEASING.md
docs/images/ banner, architecture diagram and screenshots
make check # ruff + full unittest suite
make test # python3 -m unittest discover -s tests
make lint # ruff check .
make lint-fix # ruff autofixes (imports/formatting)
make build # sdist + wheel into dist/ (needs `build`)
make deb # Debian package into dist/ (needs `dpkg-deb`)cd throtl-app
npm install # once
npm run dev # browser preview on http://localhost:1420 (mock data)
VITE_MOCK=1 npm run tauri dev # real Tauri window, mock data
npm run tauri dev # real window, live daemon data
npm run tauri build # .deb / .rpm / .AppImage (first build: 5–15 min)?static=1&theme=light&pin=27 freezes the mock and pins the graph crosshair.
Other URL parameters are handy for screenshots and deep links: theme=light|dark,
sheet=new|rule|stats|budgets|settings, pop=profile, menu=1 and
mockstate=offline|denied|connecting. To regenerate the README screenshots,
serve dist/ and capture with headless Chromium, e.g.
chromium --headless=new --window-size=1240,1000 --screenshot=docs/images/dashboard-dark.png "http://localhost:4173/?static=1&pin=27&theme=dark".
Rust is only needed for tauri dev/build; install it with
rustup and put ~/.cargo/bin on your PATH.
# lint + typecheck the frontend, and the Rust shell
cd throtl-app && npm run build && cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings && cargo test --manifest-path src-tauri/Cargo.tomlSee throtl-app/README.md for the architecture.
- The test suite needs no display and no root.
- Run
make cleanto drop build artifacts and caches.
See CONTRIBUTING.md and docs/RELEASING.md;
the project follows CODE_OF_CONDUCT.md and has a
SECURITY.md policy.
The dashboard tells you when the service is missing instead of showing an empty list:
- No process list in the GUI — the daemon could not start
nethogs. Checkthrotl-cli status(Monitor error) andsystemctl status throtl. - Limits don't seem to apply — verify the interface. Throtl auto-detects the
default-route interface; for VPN tunnels pin it explicitly:
throtl-daemon --interface tailscale0. - Prioritisation does nothing — set global download/upload caps; see How priorities behave.
ttnot found — re-runsudo ./setup/install.sh(it installs TrafficToll into/opt/throtl/venv).Permission deniedon the socket / "daemon not reachable" — the daemon socket belongs to thethrotlgroup.install.shadded you to it, but a running session keeps its old groups: runnewgrp throtlin the terminal or log out and back in.throtl-cli doctordetects exactly this.
Throtl is free software. The GPL-3.0-or-later explicitly permits commercial use, and you are not required to tell me anything. But if you do use Throtl commercially, I would honestly love to hear about it — where it ends up and how big it grows. That is pure curiosity, not a license condition.
A short note — an issue or a discussion is more than enough, and keeps my inbox clean.
Throtl is licensed under GPL-3.0-or-later (see LICENSE).
Commercial use is allowed — see A small request.
Throtl does not vendor or modify its backends; it installs and drives them as separate programs, so their licenses do not restrict Throtl's own licensing:
| Component | License | How Throtl uses it |
|---|---|---|
| TrafficToll | GPL-3.0 | installed unmodified via pip into a venv (/opt/throtl/venv) and run as a subprocess through its CLI |
| nethogs | GPL-2.0 | system package, run as a subprocess; Throtl parses its -t output |
Throtl is an independent frontend, not a fork of either project. Running a separate program and parsing its output is generally not considered creating a derivative work; even a real fork would be permitted by the GPL, but would carry the usual obligations (keep the license and notices, publish the source).
This is not legal advice.


