You edit a configuration file, run systemctl reload, get exit 0 and a green active (running), and the service carries on serving the old file. This page follows a reload through the seven steps between saving the file and a process operating on its bytes, measured on Ubuntu 24.04 against nginx, sshd, haproxy and Apache. The short version: systemctl reload reports the exit status of a command the package maintainer chose. Whether the program adopted your file is a separate event, and nothing on the machine records it.

It sits below systemctl and systemd explained, which cover the commands and the unit model. Everything here was run on systemd 255 with nginx 1.24, OpenSSH 9.6, haproxy 2.8 and Apache 2.4, all the versions noble ships.

The seven steps, and who owns each.

  1. The edit. Is the file in the include graph at all? Owned by whoever enabled it.
  2. The validator. -t, -c, configtest. Owned by the packager, who also chose where in the unit it runs.
  3. The job. systemd checks CanReload and does not re-read the unit file.
  4. ExecReload=. The packager’s command. Its exit status is what you get back.
  5. The reference. The PID that command signals, which may not be the one systemd is tracking.
  6. The handler. The program’s, and it is entitled to ignore the signal.
  7. The swap. The program parses the new bytes and either keeps the old configuration, adopts the new one, or dies.

The question you want answered is whether step 7 happened. No field stores that. systemctl show UNIT -p Type -p CanReload -p ExecReload tells you who defined the word “reload” for this unit. On every failure state measured below, it says nothing about whether the running process took the bytes.

Who defined “reload”

Of the 134 .service files Ubuntu 24.04 ships in /usr/lib/systemd/system/, eight define ExecReload=. They do not agree on what the word means.

UnitExecReload=Exit 0 means
nginxnginx -g 'daemon on; master_process on;' -s reloadA second nginx process parsed the whole configuration and sent SIGHUP to the PID in /run/nginx.pid
sshsshd -t, then kill -HUP $MAINPIDThe file parsed, a signal was delivered, and sshd did not die inside the job
haproxyhaproxy -Ws -f $CONFIG -c -q, then kill -USR2 $MAINPIDThe file parsed and a signal was delivered. Nothing waited to see what happened next
apache2apachectl gracefulapachectl returned. Nothing waited
dbusa dbus-send method callD-Bus replied
systemd-logindempty (Type=notify-reload)The daemon itself sent RELOADING=1 and then READY=1, and systemd waited for both
a hand-written unit/bin/kill -HUP $MAINPIDA signal was delivered to a PID

Exactly one of the eight ends in kill -HUP $MAINPID, and it is preceded by a validator. The received wisdom that “systemctl reload sends SIGHUP” describes SysV init scripts and hand-written units. It describes nothing Ubuntu ships. Trace nginx’s master through a reload and the signal’s si_pid is the reload client, not PID 1.

One scope note before the measurements. Every line in that table is a Debian packaging decision. Fedora and RHEL ship ExecReload=/bin/kill -s HUP $MAINPID for nginx, which inverts the nginx results below. Run systemctl cat UNIT on your own machine before trusting any of them.

Two timestamps show what the green result is a statement about. First, an ordinary successful nginx reload, from the journal with microseconds:

22:16:51.867589 systemd[1]: Reloading nginx.service ...
22:16:51.871077 systemd[1]: Reloaded nginx.service ...
22:16:51.871297 nginx[1059]: [notice] 1059#1059: signal process started

systemd logged “Reloaded” 220 microseconds before nginx’s reload client logged its first line. Second, haproxy, with a configuration that parses but whose new worker cannot bind its port:

22:19:56.372459 haproxy[1691]: [ALERT] Binding ... cannot bind socket (Address already in use)
22:19:56.372459 haproxy[1691]: [ALERT] Some protocols failed to start their listeners! Exiting.
22:19:56.372459 haproxy[1691]: [WARNING] Loading failure!
22:19:56.372807 systemd[1]: Reloaded haproxy.service - HAProxy Load Balancer.

“Reloaded” arrives 348 microseconds after “Loading failure!”. Sampled at 1, 3, 6, 12, 24, 48 and 70 seconds afterwards: ActiveState=active, SubState=running, and systemctl --failed counts zero units. journalctl -u haproxy -p err returns nothing, because [ALERT] is text and the line was logged at priority 6. Neither component is lying. systemd truthfully reports that haproxy -c passed and a SIGUSR2 was delivered, which is what the packager defined “reload” to mean for this unit. The only record of the failure is haproxy’s own socket:

$ echo "show proc" | socat /run/haproxy-master.sock -
... master 1 [failed: 1] ...

The seven steps, measured

1. The edit. A complete, valid server block written to /etc/nginx/sites-available/newsite and not symlinked into sites-enabled/. systemctl reload nginx exits 0, nginx -t passes, the worker PIDs change, and the new port answers nothing. No component reports that a file was written which nothing reads. The changed worker PIDs are the one piece of positive evidence a person tends to look for, and they mean only that a reload ran.

2. The validator. nginx.service puts its validator in ExecStartPre= and has no separate one in ExecReload. haproxy.service puts it in ExecReload= and has no ExecStartPre=. ssh.service has both. That placement decides whether a broken file is more dangerous on a reload or on a restart, and the next section shows it is opposite for nginx and haproxy.

3. The job. systemd refuses a reload on a unit with CanReload=no, loudly and with exit 3: Job type reload is not applicable for unit rsyslog.service. It refuses on a stopped unit with exit 1. It does not re-read the unit file. A new drop-in that changes ExecReload= is ignored until daemon-reload, the old command runs, and systemd prints no “changed on disk” warning for a newly created drop-in directory. It does print one when the shipped fragment itself is touched.

4. ExecReload=. Each line runs in order and the job fails on the first non-zero exit. A missing binary gives status=203/EXEC and exit 1. Prefix the same missing binary with - and systemd runs it, watches it fail, and reports Reloaded with exit 0. The - is visible in systemctl show -p ExecReload as ignore_errors=yes.

5. The reference. nginx -s reload reads /run/nginx.pid, not systemd’s MainPID. Two references to one process, maintained by two parties. Corrupt the file and leave the process alone:

/run/nginx.pid now says 99999; the real master is 3534
  [alert] 3619#3619: kill(99999, 1) failed (3: No such process)
  systemctl reload nginx  exit=1    MainPID per systemd: 3534    ActiveState=active

The reload followed the reference systemd was not using. ssh, haproxy and apache2 set no PIDFile= at all, so on those units $MAINPID is whatever systemd’s own tracking says. How a Path Becomes a File has the general form of this: holding a name is not holding the thing.

6. The handler. A daemon with signal(SIGHUP, SIG_IGN), wrapped in an ordinary hand-written unit with ExecReload=/bin/kill -HUP $MAINPID. systemctl reload exits 0, ActiveState=active, the journal says Reloading... then Reloaded. A reload that was ignored outright is byte-identical, in every diagnostic on the machine, to one that worked. Signals covers why a program is allowed to do this.

7. The swap. Where the program has a choice it keeps the old configuration. nginx keeps serving on a rejected file; haproxy’s old worker resumes; a test daemon logs CONFIG REJECTED ... KEEPING value=ALPHA. Where it has no choice it dies. sshd re-execs itself on SIGHUP and cannot re-bind a colliding ListenAddress; Apache’s graceful replaces the parent. Both leave a failed unit, and sshd leaves nothing listening on port 22.

Which of reload and restart is the safe one

The usual advice is that reload is safe and restart risks an outage. That is true of nginx on Ubuntu, for a reason the advice does not give, and it is false for haproxy on the same machine. One broken nginx file, both operations:

-- RELOAD:   exit=1  is-active=active  port 8080: 200
-- RESTART:  exit=1  is-active=failed  port 8080: 000
   22:52:51.443216 systemd[1]: Stopped nginx.service ...
   22:52:51.459475 systemd[1]: Starting nginx.service ...
   22:52:51.462199 nginx[6036]: [emerg] unknown directive "bogus" ... :97
   22:52:51.462601 systemd[1]: Failed to start nginx.service ...

The ExecStartPre validator runs after the old process has already been stopped. It does not prevent the outage; it explains it. haproxy has no ExecStartPre: restart on a broken file leaves the unit activating and the port dead, while reload on the same file exits 1 and keeps serving. For haproxy the restart is the dangerous one.

The two daemons that die on reload both pass their own pre-check first. sshd -t exits 0 on a configuration with a colliding ListenAddress; it parses, it does not bind. systemctl reload ssh then exits 1 and ss -lnt shows no listener on 22. One more sshd fact worth knowing: stock Ubuntu 24.04 enables ssh.socket, so until the first connection arrives there is no long-running sshd master to reload at all. apache2ctl configtest says Syntax OK on a colliding Listen; systemctl reload apache2 exits 0 and the unit is then failed. Which operation is safe is a per-unit fact set by where the packager put the validator, and systemctl cat UNIT is how you read it.

systemctl reload-or-restart chooses between the two for you and does not say which it chose. Against five units on this machine it reloaded nginx and logind, and silently restarted rsyslog and chrony, new MainPID and all, exit 0 throughout.

Five failures that look exactly like success

Thirteen failure states were built and every recommended check run against all of them. Ten report something, though in eight of those ten ActiveState stays active and --failed stays zero. These five report nothing: exit 0, is-active active, zero failed units, and journalctl -u UNIT -p err empty.

  1. The valid file that nothing includes (step 1).
  2. haproxy: valid on disk, refused by the process, old worker resumed. The one partial exception: haproxy writes Reload failed! into StatusText, which systemctl show haproxy -p StatusText prints and nothing else surfaces.
  3. A Type=notify-reload daemon that rejects the file and sends READY=1 anyway. This is the strongest reload protocol systemd has, and sd_notify has no verb for failure. ERRNO= and BUSERROR= before READY=1 still produce exit 0. A daemon’s only way to signal a failed reload is to never finish, which leaves the unit in reloading with a 90-second timeout that fires forever and never fails the unit.
  4. The hand-written kill -HUP unit whose daemon rejects the file. The journal order is Reloaded rl-bare.service, then SIGHUP received, then CONFIG REJECTED ... KEEPING value=ALPHA. systemd reported success first.
  5. The daemon that ignores SIGHUP (step 6).

Two checks people recommend fail in specific ways. systemctl show -p CanReload restates whether the unit has an ExecReload= line; it says yes for all five states above. -p ExecReload returns the same empty string for rsyslog (cannot reload, refuses loudly), systemd-logind (the best reload on the machine) and the notify-reload daemon in case 3.

One line in the system does record a failed reload. systemctl status after a failed nginx reload:

 nginx.service - A high performance web server ...
     Active: active (running) since Sun 2026-09-06 22:45:45 UTC; 3s ago
    Process: 5123 ExecReload=/usr/sbin/nginx ... -s reload (code=exited, status=1/FAILURE)

A green dot, active (running), and one status=1/FAILURE in a Process: line. It survives until the next reload or restart overwrites it. There is no ExecReloadTimestamp, no NReloads and no ReloadResult; a unit reloaded a thousand times and one never reloaded are indistinguishable in systemctl show.

The old worker

A reload that works is still not finished when it returns. One second after an nginx reload that exited 0:

5162 nginx: worker process is shutting down
5175 nginx: worker process
5178 nginx: worker process

The first process is serving the old configuration to every connection it still holds; the other two serve the new one. A chunked response begun before the reload completed across it, ten chunks, HTTP 200, entirely from the old generation. worker_shutdown_timeout is unset in Ubuntu’s shipped nginx configuration and nginx’s default is no timeout, so that old generation can live as long as its connections do. Verify a change with one curl straight after a reload and either generation may answer. haproxy does the same with different vocabulary (Paused frontend, Resumed frontend, and a Former worker exited line that arrives after the reload was declared done), and Apache’s graceful likewise.

Why nothing records it

Replace a shared library that a running nginx master has mapped, and the kernel keeps a record:

$ grep -c deleted /proc/2993/maps
6
7f494399d000-...  /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0.11.2 (deleted)

Now change the configuration file the same process read at startup. It is not open, it is not in /proc/PID/maps, lsof does not list it, and fuser exits 1. A mapping is a relationship the kernel has to maintain; a read() followed by close() leaves nothing behind. Keeping a Server Patched covers the library half, and it is why needrestart can exist. Nothing analogous can exist for configuration, because the kernel does not know which processes are running superseded configuration and cannot be asked.

The one heuristic left, the file’s mtime against the process start time, cannot answer the question either. A reload does not change the file’s mtime, so “edited and reloaded” and “edited and not reloaded” look the same.

What you can actually do

Three things work, and each has a cost.

Watch it happen. strace -p MASTER -e trace=openat across the reload shows the master re-opening nginx.conf, mime.types, conf.d and sites-enabled/. Needs root, a tracer, and it must be attached before the reload; it cannot be run after the fact.

Read the memory. gcore PID then strings on the dump. Works after the fact and is definitive. Costs a briefly stopped process and a multi-megabyte file.

Build the answer in beforehand. Put a marker in the configuration that the running process will hand back over its own protocol:

add_header X-Config-Generation "GEN-1" always;

$ curl -sI localhost | grep X-Config         X-Config-Generation: GEN-1
  (file changed to GEN-2, no reload)
$ curl -sI localhost | grep X-Config         X-Config-Generation: GEN-1
  (systemctl reload nginx)
$ curl -sI localhost | grep X-Config         X-Config-Generation: GEN-2

This is the only one of the three you can run in a hurry, and you cannot add it after the fact: the moment you edit the configuration to add the probe, you need a reload to make the probe true. For haproxy the equivalent is show proc on the master socket, which counts failed reloads. Bump the marker on every change, and a reload’s success becomes something you read from the process rather than infer from systemd.

Symptoms

SymptomStepCheck
Reload exited 0, service serves the old file1, 6 or 7Is the file included? systemctl status for the Process: … ExecReload line; then the program’s own interface
nginx -t passes and the reload fails2-t is an earlier parse of a file that can change; read the [emerg] line in the journal
Reload of a valid file killed the daemon7sshd and Apache re-exec or replace the parent; a bind failure is fatal. ss -lnt
Reload succeeded, -p err empty, service broken7haproxy logs [ALERT] at priority 6. journalctl -u UNIT without -p; show proc on the master socket
Two requests get two different answers after a reloadafter 7ps -o pid,args -C nginx for a worker is shutting down; set worker_shutdown_timeout
systemctl reload hangs, unit says reloading7A notify-reload daemon never sent READY=1. The 90-second timeout will not fail the unit
Changed ExecReload= in a drop-in, old command still runs3systemctl daemon-reload; no warning is printed for a new drop-in directory
reload-or-restart dropped every connection3The unit has CanReload=no; it restarted. Compare MainPID before and after
Job type reload is not applicable, exit 33No ExecReload= and not Type=notify-reload. Restart is the only option

Related reading

Leave a Reply

Your email address will not be published. Required fields are marked *