How to Fix a systemd Service That Fails to Start (2026 Guide)
This guide is for anyone staring at Active: failed on a Linux server and wanting the shortest path from that line to a running service. By the end you will be able to read systemctl status and journalctl output properly, map systemd's own exit codes (203, 217, 226 and friends) to their causes, get out of restart loops and start-limit-hit, fix environment files and permission problems, and write a unit file that fails loudly and recovers sensibly. Everything here matches the systemd v262 man pages and works on the versions shipped by Ubuntu 24.04 (255), Debian 13 and RHEL 10 (257) and Ubuntu 26.04 (259). You need root or sudo on the box; no extra packages are required.
Step 1: Read systemctl status properly
Start with the status, with full lines and no pager. Replace myapp with your unit.
# -l: do not truncate lines; --no-pager: print and exit
systemctl status myapp.service -l --no-pager
A failure looks like this (example output, trimmed):
× myapp.service - My App API
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Tue 2026-11-10 09:14:02 UTC; 8s ago
Process: 4121 ExecStart=/opt/myapp/bin/myapp --port 8080 (code=exited, status=203/EXEC)
Main PID: 4121 (code=exited, status=203/EXEC)
Three fields tell you most of the story. Loaded: shows the file systemd actually parsed; if it is not the path you edited, you are editing the wrong file or forgot a reload. Result: is the category: exit-code (process exited non-zero), signal (killed, often SIGKILL from the OOM killer), timeout (never became ready), core-dump, or start-limit-hit. And status= is the exit code. Values from 200 upward with a name such as 203/EXEC are generated by systemd itself before your program ever ran. Values from 1 to 199 came from your program.
If systemctl status prints a warning that the unit file changed on disk, run the reload before anything else:
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
Step 2: Filter journalctl to the failure
The status shows only the last ten log lines. The journal has the rest, and the useful flags are few.
# Everything from this unit in the current boot, jump to the end, with explanations
journalctl -u myapp.service -b -e -x
# Only the last 50 lines, no pager (good for pasting into a ticket)
journalctl -u myapp.service -n 50 --no-pager
# Errors and worse across the whole system in the last 15 minutes
journalctl -p err --since "15 min ago"
# The previous boot, if the box rebooted after the failure
journalctl -u myapp.service -b -1
# Kernel messages: OOM kills, segfaults, SELinux and AppArmor denials
journalctl -k -b | grep -Ei 'oom|killed process|segfault|denied'
Look for the line systemd writes when it cannot spawn the process. It always has the same shape, and the step name matches the exit code name (illustrative output in the v262 message format):
myapp.service: Unable to locate executable '/opt/myapp/bin/myapp': No such file or directory
myapp.service: Failed at step EXEC spawning /opt/myapp/bin/myapp: No such file or directory
myapp.service: Main process exited, code=exited, status=203/EXEC
myapp.service: Failed with result 'exit-code'.
If the journal for your unit is empty after a reboot, journald is probably in volatile mode. Create /var/log/journal/ and restart systemd-journald so the next failure survives; the systemd explainer covers why that is the default.
Step 3: Fix 203/EXEC (the binary could not be executed)
The systemd.exec exit code table defines 203 as a failure of the execve() call itself, most often a missing or inaccessible executable. Check the causes in this order:
# 1. Does the path in ExecStart= exist? It must be absolute.
systemctl cat myapp.service | grep ^ExecStart
ls -l /opt/myapp/bin/myapp
# 2. Is it executable, and can the service user traverse every directory?
namei -l /opt/myapp/bin/myapp
sudo chmod 755 /opt/myapp/bin/myapp
# 3. Scripts: first line must be a shebang, and no Windows line endings
head -1 /opt/myapp/bin/start.sh | od -c | head -2 # look for \r before \n
sed -i 's/\r$//' /opt/myapp/bin/start.sh
# 4. Is the filesystem mounted noexec? (common for /tmp and some /home mounts)
findmnt -T /opt/myapp/bin/myapp -o TARGET,OPTIONS
# 5. Wrong architecture (an amd64 binary on an arm64 host gives Exec format error)
file /opt/myapp/bin/myapp; uname -m
On SELinux systems (RHEL, Fedora), a binary copied from a home directory keeps the wrong label and fails with Permission denied even with mode 755. ls -Z shows the label; restorecon -v /opt/myapp/bin/myapp resets it, and ausearch -m avc -ts recent shows the denial.
One more trap: ExecStart= is not run through a shell. Pipes, &&, redirects and $(...) do not work. Either move the logic into a script or wrap it explicitly with ExecStart=/bin/sh -c '...'.
Step 4: Fix 217/USER and permission errors
Code 217 means systemd could not determine or switch to the credentials in User= (or set up user namespacing for PrivateUsers=). The usual cause is a user that does not exist yet, typically because the package that creates it has not been installed on this host, or the name has a typo.
# Does the user exist?
getent passwd myapp || echo "no such user"
# Create a system user with no login shell and no home directory
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
# Or skip the user entirely: let systemd allocate one per start
sudo systemctl edit myapp.service
# [Service]
# DynamicUser=yes
# StateDirectory=myapp
If the service starts but logs Permission denied on a file or port, test as the service user rather than as root, because root bypasses the very checks that are failing:
# Run the same command as the same user, in a transient unit
sudo systemd-run --pty --wait -p User=myapp /opt/myapp/bin/myapp --port 8080
# Ports below 1024 need a capability when not running as root
# [Service]
# AmbientCapabilities=CAP_NET_BIND_SERVICE
Directories the service writes should be owned by its user. Prefer StateDirectory=, LogsDirectory= and RuntimeDirectory=, which create /var/lib/<name>, /var/log/<name> and /run/<name> with the right owner at start, over mkdir in an ExecStartPre=.
Step 5: Fix 226/NAMESPACE (a sandbox path is missing)
226 means systemd failed to set up the mount, UTS or IPC namespace for the sandboxing directives: ProtectSystem=, ReadWritePaths=, ReadOnlyPaths=, InaccessiblePaths=, PrivateTmp= and similar. By far the most common cause is a path in ReadWritePaths= that does not exist on this host. The journal names it (illustrative):
myapp.service: Failed to set up mount namespacing: /var/lib/myapp/cache: No such file or directory
myapp.service: Failed at step NAMESPACE spawning /opt/myapp/bin/myapp: No such file or directory
The man page allows a - prefix on these paths, which makes systemd ignore them when they do not exist. Better still, let systemd create them:
[Service]
# Ignored if missing instead of failing with 226
ReadWritePaths=-/var/lib/myapp/cache
# Or create /var/lib/myapp owned by User= on every start
StateDirectory=myapp
226 also appears inside containers and some minimal VMs where the kernel or runtime does not allow new mount namespaces. If the same unit works on a normal host, that is the likely cause, and the fix is to relax sandboxing for that environment in a drop-in rather than in the main unit.
Step 6: Stop restart loops and start-limit-hit
With Restart=on-failure and the defaults, a service that crashes on startup restarts every 100ms (systemd.service), and after five starts within ten seconds (systemd-system.conf) systemd stops trying (illustrative journal lines):
myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
start-limit-hit is a symptom. The real error is earlier in the journal, in the first failure. Scroll up to it, fix it, then clear the counter:
# How many times has it restarted, and what was the last result?
systemctl show myapp.service -p NRestarts -p Result -p ExecMainStatus
# Clear the failed state and the rate-limit counter
sudo systemctl reset-failed myapp.service
sudo systemctl start myapp.service
Then make the restart policy sane, so that a dependency outage does not burn the budget in half a second. Exponential backoff has existed since v254:
[Unit]
# Rate limits are [Unit] settings
StartLimitIntervalSec=5min
StartLimitBurst=10
[Service]
Restart=on-failure
RestartSec=2s
# Back off 2s, 4s, 8s ... up to 60s
RestartSteps=5
RestartMaxDelaySec=60s
StartLimitIntervalSec= placed under [Service] is not a recognised key, so systemd logs a warning and uses the defaults. Only the legacy spellings StartLimitInterval= and StartLimitBurst= are still accepted there, according to the v262 parser table. Run systemd-analyze verify to catch it.
Step 7: Fix environment files and variables
Three things go wrong with environment configuration, and none of them produce a clear error.
First, EnvironmentFile= is not a shell script. Lines are KEY=value; export KEY=value is not valid syntax, and $(command) is not executed. If the file is missing, the service fails to start; prefix the path with - to make it optional.
# /etc/myapp/env (no 'export', no command substitution)
DATABASE_URL=postgres://myapp@db.internal:5432/myapp
LOG_LEVEL=info
Second, variables in ExecStart= use ${VAR} (passed as one argument) or $VAR (split on whitespace), and only variables systemd knows about, from Environment= or EnvironmentFile=, are expanded. Your interactive shell's environment, including PATH additions from .bashrc, does not exist for services.
Third, check what the service actually received:
# Environment as configured in the unit
systemctl show myapp.service -p Environment -p EnvironmentFiles
# Environment of the running process
sudo cat /proc/$(systemctl show -p MainPID --value myapp.service)/environ | tr '\0' '\n'
Put secrets in a file with mode 0600 owned by root, or better, use LoadCredential= so they never appear in the environment at all.
Step 8: Write a correct unit file and verify it
Most of the failures above are prevented by a unit file written with them in mind. This one is a reasonable default for a long-running network service:
# /etc/systemd/system/myapp.service (systemd 254 or newer for RestartSteps=)
[Unit]
Description=My App API
Documentation=https://example.com/docs/myapp
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=5min
StartLimitBurst=10
[Service]
# exec: start counts as failed if execve() fails, unlike simple
Type=exec
User=myapp
Group=myapp
EnvironmentFile=-/etc/myapp/env
ExecStart=/opt/myapp/bin/myapp --port 8080
Restart=on-failure
RestartSec=2s
RestartSteps=5
RestartMaxDelaySec=60s
TimeoutStartSec=30s
StateDirectory=myapp
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
Lint it, load it, start it and watch it in one go:
sudo systemd-analyze verify /etc/systemd/system/myapp.service
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
journalctl -u myapp.service -f
Type= matters for timeouts. If your daemon forks into the background, Type=exec or simple will see the parent exit and treat the service as dead; use Type=forking with PIDFile=, or better, run it in the foreground. If you set Type=notify and the program never sends READY=1, systemd waits for TimeoutStartSec= (90 seconds by default) and then reports Result: timeout.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
status=203/EXEC, "No such file or directory" |
Wrong or relative path in ExecStart= |
Use an absolute path; check with ls -l and namei -l |
status=203/EXEC, "Exec format error" |
Script without shebang, CRLF endings, wrong CPU architecture | Add #!/bin/sh, run sed -i 's/\r$//', check file vs uname -m |
status=203/EXEC, "Permission denied" |
No execute bit, noexec mount, or SELinux label |
chmod 755, findmnt -T, restorecon -v |
status=217/USER |
User= does not exist on this host |
useradd --system, or DynamicUser=yes |
status=226/NAMESPACE |
Path in ReadWritePaths= or similar is missing |
Create it, prefix with -, or use StateDirectory= |
Result: start-limit-hit |
Crashed 5 times in 10s with default limits | Fix the first error in the journal, then systemctl reset-failed |
Result: timeout |
Wrong Type= (forking or notify) or slow start |
Match Type= to the program; raise TimeoutStartSec= |
Result: signal, SIGKILL |
OOM killer, often MemoryMax= |
journalctl -k, raise MemoryMax= or fix the leak |
| Edits have no effect | Forgot daemon-reload, or editing a file under /usr/lib |
systemctl daemon-reload; use systemctl edit for drop-ins |
| Works by hand, fails as service | Missing env vars or PATH, or root-only file access |
Set Environment=; test with systemd-run -p User= |
FAQ
How do I see why a systemd service failed?
Run systemctl status <unit> -l --no-pager for the result and exit code, then journalctl -u <unit> -b -e for the full log. The line beginning "Failed at step" names the exact setup stage that failed when the exit code is 200 or higher.
What does status=203/EXEC mean in systemd?
systemd could not execute the program in ExecStart=. The file is missing, not executable, a script without a valid shebang or with Windows line endings, on a noexec mount, or built for another CPU architecture.
How do I fix "Start request repeated too quickly"?
The service hit its start rate limit (5 starts in 10 seconds by default). Find and fix the original error earlier in the journal, run systemctl reset-failed <unit>, and add RestartSec= with RestartSteps= so future failures back off instead of burning the limit.
Do I need daemon-reload after editing a unit file?
Yes. systemd keeps parsed unit files in memory, so edits do nothing until systemctl daemon-reload. systemctl edit runs the reload for you when you save.
What I check first
When a unit fails on a host I run, I read the status= value before anything else, because it splits the problem in half: 200 and above means the unit file or the host is wrong and I do not need to look at the application at all; below 200 means the program ran and I go straight to running it by hand with systemd-run -p User=. Then I check that the unit sets RestartSec=, backoff and limits in [Unit], and that systemd-analyze verify is clean in CI before the file ever reaches a server. For the full background on units, timers and cgroups, read the systemd explainer for DevOps. Official references: systemd.service, systemd.exec, systemd.unit and journalctl.
Related: How to Troubleshoot Linux Performance in 60 Seconds (2026 Checklist), Fix Kubernetes OOMKilled and Exit Code 137: Step-by-Step Guide (2026) and How to Fix SSH Permission Denied (publickey): Causes and Fixes (2026).
Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
How to Fix AWS ALB Unhealthy Targets and 502/503 Errors (2026)
Fix AWS Application Load Balancer unhealthy targets and 502, 503 and 504 errors step by step: read the health reason code, fix health check paths and success codes, security groups, ip vs instance targets, EKS readiness gates, deregistration delay, keep-alive timeouts and access logs.
13 minNov 10, 2026How to Set Up PostgreSQL Point-in-Time Recovery With pgBackRest (2026)
Step-by-step PostgreSQL PITR with pgBackRest 2.60 and PostgreSQL 18.6: an encrypted S3 repository, archive_command, full, diff and incr backups, restore to a timestamp with --type=time, verification and fixes for archive errors.
11 minOct 31, 2026How to Troubleshoot DNS with dig: NXDOMAIN, SERVFAIL, DNSSEC (2026)
Step-by-step DNS troubleshooting with dig and nslookup: compare your OS resolver, read dig output, tell NXDOMAIN from NODATA and SERVFAIL, query authoritative servers, use +trace, debug DNSSEC with +cd, EDE codes and a validator, negative TTLs and CNAME-at-apex errors.
17 min