I flashed a fresh Raspberry Pi OS Trixie Lite on a Pi Zero W the other day, and it took two and a half minutes to boot. Not to reach the desktop — there is no desktop. Two and a half minutes on a headless Lite image to get a usable network.
Startup finished in 13.420s (kernel) + 2min 20.534s (userspace) = 2min 33.955s multi-user.target reached after 1min 41.997s in userspace.
The original Zero W is an ARMv6 single core, so nobody expects miracles. But older Raspberry Pi OS releases booted this same board in a fraction of that. So I plugged in a USB-to-serial adapter and went digging.
Spoiler: it now boots in 34.8 seconds, with the network up at 29 s. No recompiling, no downgrade, no Raspberry Pi package removed. Here is where the time was hiding.
Read the critical chain, not the blame list
systemd-analyze blame is the command everybody runs, and it is the one that misleads you. It sorts services by how long they took, regardless of whether anything was waiting on them. systemd-analyze critical-chain is what actually tells you where your boot went:
multi-user.target @1min 41.997s
└─ssh.service @2min 14.804s
└─network.target @1min 40.318s
└─NetworkManager.service @51.245s +49.045s
└─dbus.service @49.121s
└─basic.target @48.914s
└─sysinit.target @48.831s
└─cloud-init-network.service @47.640s
└─cloud-init-local.service @43.980s +3.632s
└─cloud-init-main.service @24.978s +18.978s
Two culprits stacked on top of each other. cloud-init pushes sysinit.target out to 48.8 s. Then NetworkManager adds 49 s of its own on top.
Round 1: cloud-init has no business being here
Trixie ships cloud-init enabled by default. On a cloud instance that gets its identity from a metadata server, fine. On a Pi sitting on my bench with a fixed Wi-Fi config, it is 25 s of critical path plus another 31 s of cloud-final for absolutely nothing.
Most guides tell you to mask the units. I purged the package instead — but check first, because Raspberry Pi OS dependencies are full of traps:
apt-get -s purge cloud-init
Read that output carefully. If it wants to take network-manager, netplan, raspberrypi-net-mods or rpi-usb-gadget with it, stop and mask the units instead. In my case only cloud-init and rpi-cloud-init-mods were removed, so purging was safe.
That plus moving Wi-Fi from the netplan-generated runtime profile to a native /etc/NetworkManager/system-connections/*.nmconnection keyfile took me from 2m34s to 1m06s.
And then I hit the wall everybody hits.
Round 2: the 25-second NetworkManager wall
NetworkManager was still burning 23 s. The reason is well documented by now: the Raspberry Pi patched NetworkManager calls netplan generate at startup, which is 13 s of Python on an ARMv6, followed by a systemctl daemon-reload that fires every systemd generator on the box. It does this even with no netplan YAML left at all.
The two obvious fixes are both bad:
- Removing the netplan packages also removes
network-manager,raspberrypi-net-modsand friends. Not a solution. - Downgrading to the stock Debian NetworkManager means deliberately diverging from the supported Raspberry Pi packages, forever.
So I took a third route: keep every package exactly where it is, and just stop using NetworkManager. It stays installed — no broken dependencies, no downgrade — but masked, with systemd-networkd + wpa_supplicant doing the work. On a headless node with one fixed Wi-Fi network, that is all you need, and it costs about 3 s instead of 23.
This is where the serial console earned its keep. Swapping the entire network stack on a headless board is exactly the kind of thing that leaves you with a brick on the network — unless you have a cable that does not care about the network at all. I would not have attempted any of this over SSH.
Three traps on the way
The PSK in your NetworkManager keyfile is probably already hashed. Mine was 64 hex characters — the derived PMK, not the passphrase. wpa_passphrase flatly refuses it with Passphrase must be 8..63 characters. A PMK goes straight into the config, unquoted:
network={
ssid="MY-SSID"
psk=09d7a8c8… # no quotes: this is the hash, not the passphrase
}
Do not read your country code with iw reg get. It gave me DE, which had me confused for a minute — that is the country IE being advertised by the access point at runtime, not what the Pi is configured for. The real value comes from:
raspi-config nonint get_wifi_country
which correctly said FR. Get this wrong and you restrict the channels wpa_supplicant will even scan.
Masking systemd-rfkill will silently kill your Wi-Fi. It costs 6 s at boot and looks like an easy win on a board with no rfkill switch. But it is the service that *restores* the rfkill state at startup. I masked it, rebooted, and got wlan0 DOWN with this in the journal:
rfkill: WLAN soft blocked
An rfkill unblock all at runtime does not survive the reboot without it. You can keep the 6 s and still have Wi-Fi by unblocking from wpa_supplicant itself — same drop-in where you may as well kill power saving:
[Service] ExecStartPre=-/usr/sbin/rfkill unblock wlan ExecStartPost=-/usr/sbin/iw dev wlan0 set power_save off
One more thing that will bite you: systemd-resolved is not installed on Trixie Lite. If you symlink /etc/resolv.conf to its stub before running apt install systemd-resolved, you get a machine with no name resolution at all on the next boot.
And do not forget ClientIdentifier=mac in your .network file. By default systemd-networkd identifies itself over DHCP with a DUID, where NetworkManager used the MAC. Without it your router hands out a different lease and the static address you carefully reserved is gone.
1m06s → 51 s.
Round 3: the actual prize was a 14 MB initramfs
With networking sorted, the kernel was still taking 12.3 s. Here is why:
config.txt: auto_initramfs=1 /boot/firmware/initramfs: 14,233,252 bytes
Fourteen megabytes to pull off an SD card and gunzip on a 1 GHz ARMv6, plus a full udev coldplug done inside the initramfs and then done all over again on the real root.
An initramfs solves a chicken-and-egg problem: on a generic Debian PC, the NVMe driver and the ext4 driver are modules living on a disk you cannot read yet. But the Raspberry Pi kernel is built for exactly one family of hardware, and ext4 and the SD host controller are compiled into it. One command tells you:
lsmod | grep -iE "ext4|mmc|sdhost"
No output means they are not modules, means they are built in, means the initramfs is doing nothing for you. Set auto_initramfs=0 in /boot/firmware/config.txt and the kernel drops from 12.3 s to 3.98 s.
Be honest with yourself about this one though. It is the only change here that a serial console cannot rescue you from — a kernel that cannot mount its root gives you no shell at all, and recovery means pulling the SD card and editing config.txt on the FAT partition from another machine. Run the lsmod check first. And if your root is on LUKS, LVM, RAID or USB, you genuinely need that initramfs, so leave it alone.
Two smaller wins in the same pass:
/boot/firmware was on the critical path for no reason. It had pass=2, so a 2.4 s fsck.fat on every single boot, and mounting it gated local-fs.target and therefore everything downstream. Nothing at boot needs it:
PARTUUID=…-01 /boot/firmware vfat defaults,nofail,noatime,x-systemd.automount 0 0
It mounts on demand now, which is all a kernel update needs.
brcmfmac was being probed at 43.9 s. Left to udev, the Wi-Fi driver loads absurdly late. One line fixes it:
echo brcmfmac | sudo tee /etc/modules-load.d/brcmfmac.conf
Wi-Fi firmware ready at 14.4 s instead of 43.9 s.
51 s → 35.6 s.
Where I stopped, and why
The hardware was ready at 14.4 s but wpa_supplicant still did not start until 27.5 s, so I went after the ordering. I set DefaultDependencies=no. I removed its dependency on the sys-subsystem-net-devices-wlan0.device unit — the kernel netdev exists at 14.7 s, but the systemd device unit only goes active at 24.4 s waiting for udevd to drain its queue.
Combined gain from both: 2.8 seconds.
With every single dependency stripped, wpa_supplicant *still* started at 24.7 s. That is when it clicked: the bottleneck was no longer ordering at all. It was one ARMv6 core against thirty-odd units all wanting CPU at once. Past that point the only thing that helps is removing work, not resequencing it.
Worth knowing, because you can burn a lot of evenings tuning After= lines that buy you nothing.
Results
| Before | After | |
|---|---|---|
| Total boot | 2 min 33.9 s | 34.8 s |
| of which kernel | 13.4 s | 3.98 s |
| Network usable (DHCP lease) | ~1 min 40 s | 29.3 s |
| Wi-Fi latency, average ping | 415.9 ms | 9.5 ms |
That last row is not a boot metric and it surprised me most. brcmfmac enables power saving by default, and it was costing me an average of 416 ms per ping, with spikes past a full second. Turning it off took the average to 9.5 ms. If your Pi Zero W “feels” slow on the network, check that before anything else.
What I deliberately left alone
zram swap costs about 8 s of CPU at boot, but this board has 426 MiB usable and is meant to run meshtasticd. An OOM kill is worse than 8 seconds.
avahi-daemon, 4.6 s, because .local resolution is genuinely useful on a headless box.
systemd-resolved, 5.4 s. A static resolv.conf costs nothing, but then the Pi cannot follow DHCP-provided DNS if it ever moves to another network.
dtoverlay=disable-bt would free the PL011 UART, but it moves the serial console off ttyS0 — and the serial console was the whole reason any of this was safe to attempt. I only disabled the Bluetooth services.
TL;DR
If you have a Pi Zero W on Trixie booting slowly, in order of payoff:
- Purge cloud-init (check
apt-get -s purgeoutput first) auto_initramfs=0(checklsmodfirst)- Replace NetworkManager with systemd-networkd + wpa_supplicant, masking rather than uninstalling it
- Load
brcmfmacfrommodules-load.d - Take
/boot/firmwareoff the critical path - Turn off Wi-Fi power saving
- Stop tuning unit ordering — on ARMv6 you run out of CPU long before you run out of
After=lines
None of this requires diverging from the supported Raspberry Pi packages.
The underlying issues are being tracked upstream at raspberrypi/trixie-feedback#24 — worth a read if you are seeing the same thing on other models.