A default Ubuntu or Fedora install spends 20 to 30 seconds in boot work that returns nothing on a typical machine. Each of the fixes below takes one command and breaks nothing on a normal desktop or workstation.
Audit first
systemd-analyze
systemd-analyze blame | head -10
systemd-analyze critical-chain
The first command prints kernel and userspace boot times. The second lists services sorted by activation time. The third walks the dependency chain that gated default.target.
These entries dominate most reports:
NetworkManager-wait-online.service: ~8.4splymouth-quit-wait.service: ~5.0ssystemd-udev-settle.service: ~2.8sdocker.service: 10 to 20s if installedapt-daily.service/apt-daily-upgrade.service: 15 to 30s on Debian/Ubuntucups.service/cups-browsed.service: 4 to 6s combined- initramfs decompression: 3 to 13s depending on the generator
If the top of your blame output matches, the sections below return 20 to 60 seconds depending on what’s installed. No software uninstalled. No kernel recompiled.
1. Shrink the initramfs
The initramfs is a temporary root filesystem packed into RAM at boot. It loads the disk driver, decrypts LUKS if present, then pivots into the real root. It needs the drivers for your disk controller. Nothing else.
A default Ubuntu initramfs ships:
- 170 MB compressed
- 1,658 kernel modules
- 1,561 firmware blobs
- 358 network drivers
- 235 USB controllers
- 91 SCSI modules
- 85 RTC drivers
- 77 legacy ATA drivers
On a typical machine, 6,400 of 6,500 available modules are loaded into the initramfs and never used after boot. That’s 99.04% wasted decompression every boot.
Arch and Ubuntu generate generic initramfs images by default. Fedora’s dracut supports host-only generation, which produces a 3.2-second decompression versus 13 seconds for the generic image on the same hardware.
Fix on Fedora (already default in recent versions). Regenerate only the running kernel first:
echo 'hostonly="yes"' | sudo tee /etc/dracut.conf.d/hostonly.conf
sudo dracut --force /boot/initramfs-$(uname -r).img $(uname -r)
Reboot. If it works, regenerate the rest with sudo dracut --force --regenerate-all.
Fix on Arch (mkinitcpio): edit /etc/mkinitcpio.conf and replace the MODULES=() line and the autodetect hook is already in HOOKS= by default. Run:
sudo mkinitcpio -P
Fix on Ubuntu (initramfs-tools): edit /etc/initramfs-tools/initramfs.conf and set MODULES=dep, then regenerate only the running kernel’s initramfs first:
sudo update-initramfs -u -k $(uname -r)
Reboot. If it works, regenerate the rest with sudo update-initramfs -u -k all.
What can go wrong
MODULES=dep packs only the drivers your current kernel has loaded. That’s a problem in three cases:
- Same disk, different hardware. Recovery into another machine, motherboard swap, or a UEFI change that flips SATA from AHCI to RAID. The new storage controller’s driver isn’t in the initramfs and the kernel can’t find root.
- New early-boot hardware. Adding an NVMe drive, a USB boot device, or LUKS on a new disk. Anything needed before the real root mounts.
- Detection was wrong at generation time. A module wasn’t loaded when
update-initramfsran (booted withnomodeset, generated from a chroot during install) and got omitted.
update-initramfs doesn’t verify bootability. It packs files and exits. The kernel finds out at boot.
Recovery
- Boot a previous kernel. Hold Shift (BIOS) or Esc (UEFI) at boot, pick “Advanced options” in GRUB, choose the older kernel. Its initramfs is independent and still has every driver, provided you didn’t run
update-initramfs -u -k allyet. - Drop to initramfs shell. If the kernel boots but can’t find root, you land at an
(initramfs)prompt.modprobethe missing driver, thenexitto continue. Tells you which driver to add to/etc/initramfs-tools/modules. - Live USB. If every kernel is broken, boot a live image, mount root,
chrootin, setMODULES=mostback, regenerate, reboot.
2. Disable NetworkManager-wait-online
NetworkManager-wait-online and systemd-networkd-wait-online block network-online.target until a network connection is up. Default timeouts are 30 and 120 seconds. Both ship enabled on most desktop installs, even when only one network stack is in use.
The service exists for workstations that mount NFS home directories at boot. Without an NFS mount or a similar boot-time dependency on the network, the wait is dead time. Interfaces still come up after boot. Nothing else waits on the target.
sudo systemctl disable NetworkManager-wait-online.service
sudo systemctl disable systemd-networkd-wait-online.service
3. Disable plymouth-quit-wait
Plymouth is the splash screen shown during boot. It gates the rest of the boot sequence behind its own animation so the transition to the login screen looks clean. On Fedora 31 and newer, this accounts for roughly half of post-kernel boot time.
The system finishes booting in the background. Plymouth holds the transition until its fade-out completes.
plymouth-quit-wait.service has no [Install] section because it’s pulled in as a dependency of graphical.target, not enabled directly. systemctl disable won’t work. Mask it instead:
sudo systemctl mask plymouth-quit-wait.service
To reverse:
sudo systemctl unmask plymouth-quit-wait.service
To remove the splash entirely, also drop quiet splash from the kernel command line in /etc/default/grub and rerun update-grub or the distro equivalent.
4. Drop systemd-udev-settle
systemd-udev-settle was deprecated in 2013. The systemd maintainers have asked downstream packages to stop depending on it for over a decade. It still gets pulled in by multipathd, ZFS import-cache, lvm2-activation-early, and a handful of other units that haven’t been updated.
The cost is 3 to 4 seconds per boot for synchronous device enumeration that modern kernels handle asynchronously. Without multipath, ZFS, or LVM, the units pulling it in aren’t active and the service won’t run. With them, the units can usually be replaced or masked.
Check what pulls it in:
systemctl list-dependencies --reverse systemd-udev-settle.service
Mask if nothing critical depends on it:
sudo systemctl mask systemd-udev-settle.service
5. Get Docker off the critical path
docker.service typically costs 10 to 20 seconds at boot. The daemon rebuilds iptables rules, reconciles networks, and starts any containers marked restart: always. Two options depending on workflow.
Option A: socket activation (manual stack)
If you run docker compose up explicitly when starting work, defer the daemon until something talks to /var/run/docker.sock:
sudo systemctl disable docker.service
sudo systemctl enable docker.socket
The first docker command after boot takes about a second longer while the daemon comes up. Sessions where you never touch Docker pay nothing.
This breaks the “open browser, expect localhost:3000 to answer” flow, because a browser request hits your app’s port, not the Docker socket. The daemon stays asleep, the containers never start.
Option B: delayed start (always-on stack)
If you have containers running with restart: always and expect them ready at login, keep docker.service enabled but push it off the boot critical path:
sudo systemctl edit docker.service
[Unit]
DefaultDependencies=no
After=graphical.target
[Service]
ExecStartPre=/bin/sleep 5
The daemon still pays its 11 seconds, but after graphical.target is reached. The login screen draws first. Containers come up shortly after.
6. Delay apt-daily timers (Debian/Ubuntu)
apt-daily.service and apt-daily-upgrade.service are triggered by systemd timers, not by boot. The default schedule is:
[Timer]
OnCalendar=*-*-* 6:00
RandomizedDelaySec=60m
Persistent=true
The update check is scheduled for 6 AM every day. Persistent=true means that if the machine was off at 6 AM, the run gets caught up the next time it starts. So the typical developer pattern (shut the laptop at the end of the day, open it the next morning at 9) hits a missed scheduled run on every single boot. Both services fire while you’re waiting for the desktop, adding 15 to 30 seconds.
The fix is to push them past the login with a randomized delay:
sudo systemctl edit apt-daily.timer
Add:
[Timer]
OnBootSec=15min
RandomizedDelaySec=30min
Repeat for apt-daily-upgrade.timer:
sudo systemctl edit apt-daily-upgrade.timer
[Timer]
OnBootSec=15min
RandomizedDelaySec=30min
Updates still run. They just don’t race the desktop for I/O during the first minute after boot.
7. Delay CUPS
cups.service and cups-browsed.service together cost 4 to 6 seconds at boot. CUPS manages local printers. cups-browsed does network printer discovery via mDNS. On a developer machine that prints occasionally, both need to be available when you open a print dialog, but neither needs to be ready 5 seconds into boot.
Push them past the login the same way as Docker option B:
sudo systemctl edit cups.service
[Unit]
DefaultDependencies=no
After=graphical.target
[Service]
ExecStartPre=/bin/sleep 5
Repeat for cups-browsed.service:
sudo systemctl edit cups-browsed.service
[Unit]
DefaultDependencies=no
After=graphical.target
[Service]
ExecStartPre=/bin/sleep 5
Printers still work. The GTK “Print to File (PDF)” option still works. The discovery just happens after the desktop is up, not before.
Minor wins
Two more that show up on default installs and are usually safe to drop on a modern bare-metal developer machine.
rng-tools / haveged. Entropy daemons that existed because the old /dev/random would block at boot waiting for kernel entropy. Linux 5.6 (2020) made getrandom() non-blocking once the CRNG is seeded, which happens within milliseconds on any CPU with RDRAND (every modern x86 and ARM). On bare metal, these daemons no longer do anything useful. On a VM without paravirtualized entropy (virtio-rng), keep them.
sudo systemctl disable --now rng-tools.service
sudo systemctl disable --now haveged.service
fwupd-refresh.service. Downloads the LVFS firmware metadata catalog. It doesn’t apply firmware. That’s fwupdmgr update, run manually. Masking it means you fetch the catalog yourself before checking for updates.
sudo systemctl mask fwupd-refresh.service
Each saves under a second on most installs. Worth doing once, not worth measuring twice.
Past advice that aged poorly
Every list of Linux boot tweaks recycles the same handful of suggestions from the spinning-rust era. Most of them stopped mattering 10 to 15 years ago. Worth naming them so you don’t waste an afternoon.
noatimemount option. The default isrelatime(since kernel 2.6.30, 2009), which already eliminates almost all atime writes. On an NVMe SSD doing 500k IOPS, the difference betweenrelatimeandnoatimeis microseconds per read. Won’t show up in any boot measurement.rootfstype=ext4on the kernel cmdline. Skips the filesystem probe. The probe reads a few hundred bytes of superblock magic; cost is under a millisecond. Adds a brittle cmdline parameter that breaks silently if you ever migrate the root filesystem.quiet loglevel=0. Suppresses kernel messages on the console. On a serial console this used to matter because writing to a UART was slow. On a modern framebuffer the console driver coalesces output and the cost is negligible. If you followed section 3 and dropped the splash, you’ll see the messages scroll. That’s the point, and rendering them isn’t what’s slow.- Disable IPv6. Doesn’t save measurable boot time and breaks more than it fixes on a modern network stack. Cargo-culted from 2010-era forum posts.
ureadahead/preload. Ubuntu shippedureadaheadto pre-fetch boot files into page cache. It helped on 5400 RPM laptop drives. Ubuntu dropped it in 20.04 because on SSDs it was at best a wash and sometimes slower than letting the kernel’s own readahead work. Don’t reinstall it.- Switching init systems.
runit,OpenRC,s6. All faster at starting individual units. None of them help when the actual cost iswait-onlineblocking for 30 seconds, Plymouth gating to its own framerate, orapt-dailycatching up missed runs. The slow services aren’t slow because of systemd.
The pattern: most of these tweaks come from a time when boot was I/O-bound on slow disks. Boot today is dependency-bound on services that wait for things. Optimizing the wrong layer.
Measure the result
Reboot once and rerun systemd-analyze. Compare against the number you started with. If critical-chain still flags a service over 3 seconds, it’s a candidate for the same treatment: find what pulls it in, decide if you need it at boot, and either delay it or move it off the critical path.
Leave a Reply