Per-application bandwidth limits and traffic prioritisation for Linux.
Throtl brings fine-grained bandwidth control to Linux/Omarchy: set per-application download/upload limits and traffic priorities from a native GTK4 + libadwaita app, or from a scriptable CLI — without reinventing the shaping engine.
It is a thin, well-behaved layer on top of two proven tools:
- TrafficToll (
tt) does the actualtc/cgroup shaping and runs as a managed subprocess. - nethogs in trace mode provides live per-process bandwidth.
Live view: per-app rates, editable limits and priority, auto-scrolling graph.
Throtl follows your system style by default and can be pinned from the main menu:
- Per-application limits — download/upload caps for a single app, even when it runs many processes (they are grouped into one row and summed).
- 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.
- Priorities — Critical / High / Normal / Low for individual apps, plus a global default priority for everything that has no rule.
- 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.
- Global switch — turn all shaping on/off without losing your rules.
- Consumption budgets — a rolling daily/weekly volume limit (global or per app); the GUI warns and shows a desktop notification when it is exceeded.
- Budgets, editable in the app — main menu → Budgets… sets the global daily/weekly volume and per-application budgets, showing how much is already used. A warning arrives at 80 % of a budget, so there is still time to react.
- Explains itself — every affected row says which rule applies (own limit, global limit, time window and whether it is active right now).
- 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.
- Statistics — persistent per-app history over 1 h / 2 days / 30 days, shown as a table and a graph.
- 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 GNOME design — built on libadwaita and following your system light/dark preference (or pin Light/Dark in the main menu); the graph and rate colours adapt with it.
- 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).
- Update notice — tells you when a newer release exists and opens its page in your browser. Throtl never installs anything by itself.
- 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 (developed on Arch / Omarchy, Wayland)
- Python 3.11+
nethogs,gtk4,libadwaita,python-gobject,python-cairo(all in Arch[extra])traffictoll— installed by the setup script into a venv under/opt/throtltcand theifbkernel module (for ingress shaping)
The backend itself has no third-party Python dependencies; the GUI uses the system PyGObject.
git clone https://github.com/Flenser42/throtl.git
cd throtl
sudo ./setup/install.shThe script:
- installs the system packages listed above,
- creates
/opt/throtl/venvand installstraffictoll, - copies the code and launchers to
/opt/throtl(and symlinksthrotl-cli/throtl-gui/throtl-daemoninto/usr/local/bin), - creates
/etc/throtl/config.tomland/run/throtl, - installs and starts the
throtlsystemd service, - installs the desktop entry, the icon and offers autostart.
Uninstall with sudo ./setup/uninstall.sh (add --purge to also remove code
and configuration).
Prebuilt artifacts / distro packages: Throtl is pure Python, so there is nothing to compile. Every release carries an sdist, a wheel and a Debian
.deb(build it locally withmake deb), and there is an AURPKGBUILDfor Arch/Omarchy. Seepackaging/README.md.
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:
/opt/throtl/bin/throtl-guiThe window gives you a global on/off switch, the active profile, global limits
and priority, the live graph and the per-app table. Type a limit into a row's
DL limit / UL limit field (empty = unlimited) and pick a priority; the change
is sent to the daemon automatically.
Each row explains itself: underneath an application that is affected by
something, a quiet line says why — Your rule: 0.5 MB/s download, 0.062 MB/s upload or Your rule: 0.04 MB/s download · Mon-Fri 20:00-00:00 (not active now). Rows that are unlimited stay one line, so only the interesting ones
grow. The ⋯ button on a row opens its actions: set a budget for exactly that
application, or edit its time window.
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 |
|---|---|
Ctrl+R |
Reload settings and the process list |
Ctrl+I |
Open Statistics |
Ctrl+S |
Save the current settings as a profile |
Ctrl+F |
Focus the application filter |
Esc |
Clear the filter |
Ctrl+1 … Ctrl+9 |
Activate the n-th profile |
Alt+D / Alt+U / Alt+P |
Jump to the download limit, upload limit, priority field |
On start, Throtl asks the public GitHub release API which version is current and shows a banner when yours is older — “Throtl 0.9.0 is available (installed: 0.8.0)” — with a Download 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 the main menu under Updates → Check for updates on start; Check for updates runs it once on demand.
The daemon is fully controllable without the GUI:
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
# 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).
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 GUI exposes this under Statistics… (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.
- 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)
gui/ GTK4 + libadwaita frontend (app, graph, process table,
rule-window dialog, prefs, style.css)
tests/ unittest suite (GUI tests run under Xvfb)
setup/ install/uninstall, systemd unit, .desktop, autostart
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 (GUI tests skip headless)
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`)
# Run the GUI widget tests on a headless machine
xvfb-run -a -s "-screen 0 1280x900x24" make test
# Regenerate the README screenshots / demo GIF (uses the Xvfb trick above)
make images- Prefer the system Python for GUI tests (
/usr/bin/python3), since a virtualenv typically lacks PyGObject. TheMakefiledoes this automatically. - GUI widget tests and
make imagesare skipped when no display is available. - 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 window tells you when the service is missing instead of showing an empty table:
- 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.







