A small macOS menu bar app that shows live download and upload speed.
Live throughput, stacked two-line by default — download on top, upload below.
No Dock icon, no window — it lives entirely in the menu bar (LSUIElement).
Pure AppKit, no third-party dependencies.
Download NetSpeedBar 1.0.0 — universal binary, macOS 13 or later
Unzip, move it to /Applications, then clear the quarantine flag macOS attaches to
downloads:
xattr -dr com.apple.quarantine /Applications/NetSpeedBar.app
open /Applications/NetSpeedBar.appThat step is not optional. The app is ad-hoc signed rather than notarized,
so Gatekeeper refuses to open it and reports it as damaged. It is not damaged —
the bundle passes codesign --verify --strict; the message is what macOS shows
for any un-notarized app that arrived with a quarantine flag.
Building from source avoids this entirely, since a locally built copy is never quarantined.
./build.sh
open dist/NetSpeedBar.appbuild.sh produces a universal (arm64 + x86_64) release binary, wraps it in
dist/NetSpeedBar.app, and ad-hoc signs it.
To install it properly:
cp -R dist/NetSpeedBar.app /Applications/Launch at login can be toggled from the menu, or scripted — registration has
to run from inside the app, and it registers whatever copy it is run from, so
run the one in /Applications:
/Applications/NetSpeedBar.app/Contents/MacOS/NetSpeedBar --enable-login-item--disable-login-item and --login-item-status are also accepted.
Click the readout to get:
- The connections carrying traffic, by friendly name — e.g.
Wi-Fi (en0). Ports that are up but idle (Thunderbolt bridges, empty Ethernet adapters) are filtered out, so this names the link in use rather than every port - Session totals, with a reset
- Display — stacked (two lines) or single line
- Units — bytes (
MB/s) or bits (Mbps) - Update Every — 1, 2 or 5 seconds
- Launch at Login
- Open Activity Monitor / Quit
Settings persist in UserDefaults.
Byte counters come from sysctl(NET_RT_IFLIST2), and speed is the delta
between two samples divided by the real elapsed time.
Two details worth knowing:
-
NET_RT_IFLIST2, notgetifaddrs. Thegetifaddrscounters are 32-bit and wrap every 4 GB — under a minute on a gigabit link.NET_RT_IFLIST2carries the 64-bitif_data64totals instead. -
Physical interfaces only (
en*,pdp_ip*,ppp*). VPN tunnels (utun*) are deliberately excluded: that traffic also crosses the physical interface, so counting both would double every byte. AirDrop-style peer-to-peer links (awdl*,llw*) are excluded as not-internet. -
Deltas are per interface, then summed — never taken on a pre-summed total. An interface appearing mid-session (a dock plugged in, Wi-Fi switched back on) arrives carrying its whole lifetime counter; subtracting summed totals would report that as one enormous burst that never happened. An interface with no previous reading is skipped for that tick and baselined for the next.
Samples taken across a gap longer than 10 seconds are discarded rather than reported, so waking from sleep doesn't render as a huge spike.
The icon is drawn in code rather than checked in as an opaque binary, so it can be adjusted and re-rendered:
swift Tools/GenerateIcon.swiftThat writes Resources/AppIcon.icns (every size from 16 to 1024, at 1x and
2x) plus build/icon-preview.png for eyeballing. build.sh regenerates it
automatically if the .icns is missing, and copies it into the bundle.
Because this is a menu bar app with no Dock presence, the icon shows up in Finder, Get Info, and System Settings › General › Login Items.
The readout is drawn into a template image, not set as the status button's title. Fitting two lines into a title requires clamping the line height, and that clamping pins the block to the top of the bar rather than centring it — 1.5 pt of space above and 10.5 pt below, measured. AppKit centres an image on its own, and a template image is tinted by the system, so light/dark and the click highlight are handled for free.
Vertical ink, measured against the real 30 pt menu bar:
| above | below | |
|---|---|---|
| title-based (before) | 1.5 pt | 10.5 pt |
| image-based (now) | 7.5 pt | 6.5 pt |
| a neighbouring system icon | 7.0 pt | 7.0 pt |
Note that NSStatusBar.system.thickness reports 22 pt — that is the item
thickness, not the bar. The bar itself is 30 pt here, from
NSScreen.frame.maxY - visibleFrame.maxY.
Two separate things control how much room the readout takes:
- The item's own width, sized to the text actually being drawn rather than to the widest string it could ever show.
- The padding AppKit reserves around it, trimmed via
NSStatusItemSpacing/NSStatusItemSelectionPadding, written to this app's current-host preference domain at launch. This is not the system-wide setting — every other menu bar item keeps its normal spacing.
Measured from a screenshot (2x pixels, threshold on glyph brightness):
| gap after the readout | gap between two system icons | |
|---|---|---|
| default spacing | 21.0 pt | 22.5 pt |
| trimmed | 13.0 pt | 22.5 pt |
To go tighter still you would have to compress every item, which is a system-wide change and affects the whole menu bar:
defaults -currentHost write -globalDomain NSStatusItemSpacing -int 6That one needs a log out and back in, and is reverted with
defaults -currentHost delete -globalDomain NSStatusItemSpacing.
netstat -ibReading the same interval both ways agrees: ~1.1 MB/s in the menu bar against
1015 KB/s of en0 bytes-in over the sampling window.
- The signature is ad-hoc, which is fine on this Mac. Copying the app to another machine would need a Developer ID signature and notarization.
- Launch at Login uses
SMAppService, which needs a real app bundle — it will not work if you run the bare binary out of.build/.
One of three small macOS menu-bar utilities, shown here running side by side:
- NetSpeedBar — live download and upload speed ← you are here
- DisplayHelper — keeps the display awake and the system marked active
- PowerToggleBar — one-click battery saver with exact restore
MIT — see LICENSE.


