Paste your systemctl status output instead
The systemctl status Explainer reads systemctl status or journalctl -u output in your browser and names the cause line by line, for 203/EXEC and every other systemd failure state. Nothing is uploaded.
A new service fails with status=203/EXEC, and the program's own log is empty because it never ran. The usual advice is "use an absolute path", which fixes one of the five causes. The other four produce the same 203/EXEC in systemctl status, and the journal line that tells them apart is two lines above the one people search for.
What Does status=203/EXEC Look Like?
Reproduced on 2026-10-10 on this machine (Kali, systemd 261) with throwaway user units, so no sudo was needed; system units behave the same and log the same lines. The user name is shown as travis and the home directory as ~. A script with no #! line:
Note the first two lines: systemctl start exited 0. For Type=simple, the default, start returns once the service is forked, before the exec fails. Every simple unit on this page did the same; only a Type=oneshot unit made start print Job for ... failed. A deploy script that trusts the exit code of systemctl start reports success for a service that never ran.
Which of the Five Causes Is It?
Five units, one fault each, same status=203/EXEC in every systemctl status. The line that differs is the one from the process name in parentheses:
| Fault | Journal line before status=203/EXEC |
|---|---|
ExecStart=noshebang.sh (relative path) | Unable to locate executable 'noshebang.sh': No such file or directory |
| path to a file that does not exist | Unable to locate executable '~/demo/svc/nope.sh': No such file or directory |
| file without the execute bit | Unable to locate executable '~/demo/svc/noexec.sh': Permission denied |
script with no #! line | Failed to execute ~/demo/svc/noshebang.sh: Exec format error |
#!/usr/bin/python9 (interpreter missing) | Failed to execute ~/demo/svc/badinterp.sh: No such file or directory |
#!/bin/bash saved with CRLF endings | Failed to execute ~/demo/svc/crlf.sh: No such file or directory |
The last two are the confusing ones. The file exists, and the error says it does not: what is missing is the interpreter, /usr/bin/python9 or /bin/bash\r with an invisible carriage return. That is the same fault as /bin/bash^M: bad interpreter in a shell, without the helpful ^M.
A noexec mount gives the same Permission denied line as a missing execute bit; see Fix Bash "Permission denied" for how to tell those two apart.
Two neighbours fail at a different step and get a different code:
Those show status=200/CHDIR (WorkingDirectory= missing) and status=217/USER (User= does not exist). systemd-analyze exit-status prints the whole table on any box; on this one, systemd 261 lists 46 codes from 200 to 255.
The Script
Save as execstart-check.sh. Point it at a unit (add --user for a user unit) and it checks ExecStart=, WorkingDirectory= and User= the way the kernel and systemd will, using what systemd actually loaded rather than the file on disk.
Prerequisites
bash 4 or later, systemd (systemctl show), util-linux (findmnt) and getent. No root for user units or for reading system units, but the script must be able to read the executable to check its #! line, so run it as root or as the service's user for system services.
How Does Each Check Work?
NeedDaemonReloadisyeswhen the unit file changed after systemd loaded it. Every fix you make to the file does nothing untilsystemctl daemon-reload, which is the most common reason a "fixed" unit fails the same way.systemctl show -p ExecStartreturns the parsed command, withpath=holding the program systemd will run. A path that does not start with/is the relative-path cause.findmntbefore-xreportsnoexecfirst, because on anoexecmount even arwxfile is not executable andchmod +xwould be the wrong advice.- The
#!check skips ELF binaries, flags a text file without#!(Exec format error), then checks the interpreter. A#!line ending in\ris reported as CRLF instead of as a missing interpreter, because that is the fix you need. WorkingDirectory=andUser=cover 200/CHDIR and 217/USER. A leading-(optional directory) and the user manager's own!default are skipped.
What Does the Script Print?
Against the units above, plus a working one:
Run it before systemctl enable --now on a new unit, and the first start is not the test.
Once It Starts, What If It Keeps Dying?
A program that runs and then exits non-zero is no longer 203: it is status=1/FAILURE (or its own code), and with Restart= set it can turn into Start request repeated too quickly. For keeping a service up once it works, Auto-Restart a Linux Service covers Restart=, watchdogs and alerting.
Frequently Asked Questions
What does status=203/EXEC mean in systemd?
203 is systemd's EXEC exit status: it set up the service and called execve() on the ExecStart= program, and the kernel refused. Your program never ran, so its own logs are empty. The cause is in the unit file or the executable: a relative or wrong path, a missing execute bit or noexec mount, a script without #!, or a #! interpreter that does not exist (including a CRLF line ending).
Why does my script run from the shell but fail with 203/EXEC in systemd?
The shell is more forgiving than execve(). bash runs a file with no #! line itself, and finds a relative name through your PATH. systemd does neither: it needs an absolute path in ExecStart=, an executable file, and a #! line naming an interpreter that exists. Add #!/bin/bash as line 1, chmod +x the file and use the full path.
How do I see the real error behind 203/EXEC?
Run journalctl -u UNIT -n 20 (add --user for a user unit). The line just before Main process exited names the cause: Unable to locate executable with No such file or directory or Permission denied, or Failed to execute with Exec format error or No such file or directory. systemctl status shows the same lines at the bottom when they are recent.
Why does systemctl start succeed when the service fails with 203/EXEC?
For Type=simple, the default, systemctl start returns as soon as systemd has forked the service, before the exec happens, so it exits 0 even when the program then fails with 203/EXEC. In the tests for this page every simple unit returned 0; only the Type=oneshot unit printed Job for ... failed. Check systemctl is-active or systemctl status after starting.
What is the difference between 203/EXEC and 200/CHDIR or 217/USER?
They are all systemd setup failures before your program runs, each from a different step. 200/CHDIR means WorkingDirectory= does not exist, 217/USER means User= names an account that does not exist, and 203/EXEC means the final exec failed. The journal names the step in Failed at step EXEC, CHDIR or USER, and the systemctl status Explainer decodes all of them.
Part of the bash snippets collection
Related Scripts
- Fix systemd "Start request repeated too quickly" — when the program starts and keeps dying
- Fix /bin/bash^M: bad interpreter — the CRLF cause, from the shell's side
- Fix Bash "Permission denied" (Exit 126) — execute bits, noexec mounts and directories
- Restart Service If Stopped — a watchdog for services that do start
- Auto-Restart a Linux Service — Restart=, start limits and alerting, end to end