systemctl start myapp printed nothing. The service is dead anyway.
That is normal for most services. A Type=simple service counts as started
the moment systemd forks it. If the binary is missing, or the User= does not
exist, systemctl start still reports success. The failure happens a
millisecond later, and nobody tells you.
So the first step is never “start it again”. It is to ask systemd what happened.
systemctl statussudo systemctl status myapp
Most of the output is noise. Two lines carry the answer:
Active: failed (Result: exit-code) since Sat 2026-09-26 09:14:02 UTC; 3min ago
Main PID: 4121 (code=exited, status=203/EXEC)
Result: says what kind of failure it was. status= says how the process
ended. Together they usually name the problem before you read a single log
line.
Run it with sudo. status shows the last 10 journal lines for the unit.
An ordinary user often sees no lines at all, and gets no warning that
anything is hidden.
journalctl -xeu showsThe error message tells you to run journalctl -xe. Add -u and the unit
name:
sudo journalctl -xeu myapp
Each letter does one thing:
| Flag | What it does |
|---|---|
-u myapp |
This unit’s own lines, plus the lines systemd logged about it |
-e |
Jumps to the end of the pager. Also limits output to 1000 lines and the current boot |
-x |
Adds explanation text to some systemd messages |
The -e limit catches people. After a reboot, journalctl -xeu myapp cannot
show a failure from before it. Ask for the previous boot:
sudo journalctl -u myapp -b -1 -e
The -x text is generic. It explains what “Failed with result” means, not
why your service did it. Leave it off when you paste output into a bug report
or a chat. The systemd manual asks for that too.
The lines you are looking for come from systemd[1], not from your program:
systemd[1]: myapp.service: Main process exited, code=exited, status=203/EXEC
systemd[1]: myapp.service: Failed with result 'exit-code'.
Your program’s own output, if it got far enough to print any, sits just above them.
The word in Result: narrows the search.
| Result | What happened | Where to look |
|---|---|---|
exit-code |
The process exited with a non-zero code | The status= number, next section |
signal |
A signal killed it, with no core dump | Who sent it: a script, kill, a stop command |
core-dump |
It crashed and dumped core | coredumpctl list |
oom-kill |
The kernel’s OOM killer ended it | Memory limits, MemoryMax=, the host |
timeout |
A start or stop step ran out of time | TimeoutStartSec=, next-but-one section |
start-limit-hit |
It failed too often, too fast | The first failure, not this one |
watchdog |
It stopped pinging the watchdog | The program hung |
protocol |
It broke the rules of its Type= |
Type=forking or notify set wrong |
exec-condition |
ExecCondition= said no |
The condition command |
resources |
systemd could not fork or set up the process | The systemd[1] lines just above it |
A status from 1 to 199 came from your program. Read its own log lines.
A status from 200 to 243 is different. Programs rarely use these numbers. They mean systemd failed while setting the process up, before your program ran a single instruction. Your program has no logs to read, because it never started.
The ones you will meet:
| Status | Name | What went wrong |
|---|---|---|
200 |
CHDIR |
WorkingDirectory= does not exist |
203 |
EXEC |
ExecStart= could not be executed |
209 |
STDOUT |
The StandardOutput= target could not be opened |
216 |
GROUP |
The Group= could not be set |
217 |
USER |
The User= could not be set, usually because it does not exist |
226 |
NAMESPACE |
Sandboxing setup failed, often a path in ReadWritePaths= that does not exist |
233 |
RUNTIME_DIRECTORY |
RuntimeDirectory= could not be created |
238 |
STATE_DIRECTORY |
StateDirectory= could not be created |
203/EXEC is the most common by far. Check these in order:
chmod +x.#! line. The kernel cannot run it.noexec.ExecStart= takes an absolute path or a bare command name. A bare name is
searched in a fixed list of directories chosen when systemd was built, not in
your shell’s PATH. /opt/myapp/bin is never on it.
start-limit-hit hides the real errorThis one reads like the problem. It is not.
systemd allows 5 starts in 10 seconds by default. A service with
Restart=always that crashes on startup burns through that in a moment. Then
systemd stops trying:
systemd[1]: myapp.service: Start request repeated too quickly.
systemd[1]: myapp.service: Failed with result 'start-limit-hit'.
The cause is the first failure, five attempts earlier. Scroll up, or search for it:
sudo journalctl -u myapp -b -g 'Failed with result'
Fix that, then clear the counter so systemd lets you start it straight away:
sudo systemctl reset-failed myapp
sudo systemctl start myapp
timeout depends on the service typeThe default start timeout is 90 seconds. Only some types can hit it.
The other types wait for something before they count as started. notify
waits for the program to report READY=1. forking waits for the parent to
exit. dbus waits for the bus name. If the wait runs past
TimeoutStartSec=, systemd gives up and kills the process.
Type=simple never times out on start, because systemd does not wait for
anything. Type=oneshot has no start timeout by default.
A notify service that times out has usually been told it is notify when
it does not speak the protocol. Either fix the Type= or raise
TimeoutStartSec= for a program that is slow to warm up.
systemd[1]: Dependency failed for My App.
Nothing is wrong with your service. Something it Requires= failed first.
List every failed unit on the machine:
systemctl --failed
Then start this guide again with that unit’s name.
Most of the setup failures above are visible in the unit file. Let systemd read it first:
sudo systemd-analyze verify /etc/systemd/system/myapp.service
It reports unknown directives, missing dependencies, and an ExecStart=
command that is missing or not executable. That last one is 203/EXEC, caught
before it happens.
After editing a unit file, run sudo systemctl daemon-reload. Without it,
systemd keeps using the version it loaded last.
Everything above assumes you know which service failed, on which machine.
On one server that is fine. On ten, a service that died at 3 a.m. stays dead
until somebody notices the thing it served is broken. systemctl --failed is
accurate. It just has to be run on every host, by someone.
Point the CL Agent at a Central Logging instance. It ships
each journal entry as the JSON object journalctl -o json prints, every field
included.
That matters here, because of where systemd files its failure messages.
They are logged by PID 1. Their _SYSTEMD_UNIT is systemd’s own, not your
service’s. The service name is in a separate field, UNIT. journalctl -u
quietly searches both. A query has to do the same, or it misses exactly the
lines you want:
SELECT json_extract(msg, '$.MESSAGE') AS message
FROM logs
WHERE json_valid(msg)
AND (json_extract(msg, '$._SYSTEMD_UNIT') = 'myapp.service'
OR json_extract(msg, '$.UNIT') = 'myapp.service');
Filter on _SYSTEMD_UNIT alone and you get the program’s output with the
failure cut out.
The “Failed with result” line also carries a fixed MESSAGE_ID. It is the
same on every systemd machine, and it is logged for every kind of failure.
“Failed to start” is not: a Type=simple service has already “started”
before its binary fails to run, so that line may never appear. Key on the ID
instead. Every service failure across the fleet, last 24 hours:
SELECT datetime(CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000, 'unixepoch') AS at,
json_extract(msg, '$._HOSTNAME') AS host,
json_extract(msg, '$.UNIT') AS unit,
json_extract(msg, '$.UNIT_RESULT') AS result
FROM logs
WHERE json_valid(msg)
AND json_extract(msg, '$.MESSAGE_ID') = 'd9b373ed55a64feb8242e02dbe79a49c'
AND CAST(json_extract(msg, '$.__REALTIME_TIMESTAMP') AS INTEGER) / 1000000
>= CAST(strftime('%s', 'now', '-1 day') AS INTEGER)
ORDER BY at DESC;
UNIT_RESULT holds the same word systemctl status shows after Result:.
Keep the CASTs. journalctl’s JSON quotes every value, and SQLite compares
text to a number without complaint and gets it wrong.
To be told instead of asking, create an alert rule with alert on results:
SELECT 1 FROM logs
WHERE timestamp > CAST(strftime('%s','now','-10 minutes') AS INTEGER)
AND json_valid(msg)
AND json_extract(msg, '$.MESSAGE_ID') = 'd9b373ed55a64feb8242e02dbe79a49c'
LIMIT 1
Rules run every five minutes. The ten-minute window stops a failure that lands on the boundary from slipping between two runs. Set Max Frequency to at least 15 minutes, or a service in a restart loop pages you on every run.
A job you run on purpose and expect to fail sometimes can be excluded by name:
AND json_extract(msg, '$.UNIT') NOT IN ('certbot.service')
systemctl status is enoughOne server, and you are the one who restarts things? systemctl status and
journalctl -xeu are the whole toolkit, and this page covers them. Stop here.
Shipping the journal starts to pay at the second host, or at the first failure nobody saw.
-u in depth, and the five reasons it prints nothing.--list-boots and -b -1, for failures that happened before the reboot.-p err, and why a program’s errors can hide at info.-g, and matching on fields such as UNIT instead.journalctl -xeu itself prints nothing.💌 Get notified on new features and updates