Start Request Repeated Too Quickly: Debian 13

11 min read

A service won't come up. systemctl status shows a red "failed" and three lines that tell you almost nothing on their own. The usual forum advice is to raise StartLimitBurst until the error goes away. That only hides the problem: the unit keeps crashing, it just stops telling you about it. This article shows how to read the real cause of a systemd "start request repeated too quickly" failure on Debian 13 (trixie, systemd 257). It maps the exit status systemd reports to the setting that broke, then shows how to test a fix without guessing.

Tested on 2026-10-28 on a fresh Debian 13.7 VM (arm64, systemd 257.13-1~deb13u1): the exit codes, journal lines and start-limit behaviour below are real output from that test.

The error messages

A typical failure looks like this in systemctl status myapp.service and journalctl -xeu myapp.service:

× myapp.service - My App
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Wed 2026-10-28 22:30:59 UTC; 1s ago
   Duration: 44ms
 Invocation: c3b3b5e22fd14ecab7cab96aa8098ce8
    Process: 939 ExecStart=/usr/local/bin/myap (code=exited, status=203/EXEC)

myapp.service: Unable to locate executable '/usr/local/bin/myap': No such file or directory
myapp.service: Failed at step EXEC spawning /usr/local/bin/myap: No such file or directory
myapp.service: Main process exited, code=exited, status=203/EXEC
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'exit-code'.
Failed to start myapp.service - My App.

A unit that depends on it fails with a different message:

Dependency failed for myapp-worker.service - My App Worker.
myapp-worker.service: Job myapp-worker.service/start failed with result 'dependency'.

"Start request repeated too quickly" is never the cause. On Debian 13 the unit keeps the result of its last real failure, so systemctl status shows Result: exit-code, not start-limit-hit. systemd's $SERVICE_RESULT documentation does list start-limit-hit, and many forum posts show it, but in my test on systemd 257.13 it never appeared. A manual systemctl start while the limit is active is also misleading: it fails with "the control process exited with error code", although nothing was started. The useful information is the line before the limit message: exit-code plus a status= value. Scroll up in the journal to the first failure, not the last one.

Quick fix for the most common cause

On self-written units the top cause is status=203/EXEC: systemd could not execute the binary at all. Check the path and the execute bit, fix the unit, then clear the failed state:

systemctl cat myapp.service
ls -l /usr/local/bin/myapp
head -n1 /usr/local/bin/myapp
systemd-analyze verify /etc/systemd/system/myapp.service

The first argument of ExecStart= must be an absolute path or a plain file name that systemd resolves via its compiled-in search path (/usr/local/bin, /usr/bin and their sbin variants). Shell syntax like pipes, && or redirections doesn't work in ExecStart= without an explicit /bin/sh -c. If the file is a script, its shebang interpreter must exist too.

After the fix, reload and start. This restarts the service, so do it when a restart is acceptable:

systemctl daemon-reload
systemctl reset-failed myapp.service
systemctl start myapp.service

According to systemctl(1), reset-failed clears the failed state and also resets the start rate limit and restart counters. Without it, a unit that hit the limit refuses new starts until the interval has passed.

Exit status to cause table

The numbers from 200 upward are systemd's own codes from systemd.exec(5). They mean systemd failed while setting up the process, before your program ran any code. Code 1 is your program's own generic failure.

Statussystemd meaningTypical cause on a real server
203/EXECThe execve() call failedWrong ExecStart= path, missing execute bit, broken shebang, or the binary sits in a path a sandbox option hides (e.g. /home with ProtectHome=yes)
200/CHDIRChanging to the working directory failedWorkingDirectory= doesn't exist or isn't reachable for User=
217/USERFailed to determine or change user credentialsUser= names an account that doesn't exist (typo, or the package that creates it isn't installed)
216/GROUPFailed to determine or change group credentialsGroup= or SupplementaryGroups= names a group that doesn't exist
226/NAMESPACEFailed to set up mount, UTS or IPC namespacingReadWritePaths=, ReadOnlyPaths= or BindPaths= point to a path that doesn't exist
233/RUNTIME_DIRECTORYSetting up the runtime directory failedProblem creating the RuntimeDirectory= below /run
238/STATE_DIRECTORYSetting up the state directory failedProblem creating StateDirectory= below /var/lib
1/FAILUREGeneric failure from the programThe app itself gave up: bad config, port in use, or it can't write a file because ProtectSystem=strict made the path read-only

The full list (including 209/STDOUT, 228/SECCOMP and 243/CREDENTIALS) is in the "Process Exit Codes" section of systemd.exec(5).

Diagnosing each cause

Read the journal lines before the failure

Show only this unit's messages since the last boot with the catalog explanations. The lines just above "Main process exited" name the step and the errno:

journalctl -b -x -u myapp.service --no-pager -o short-precise
systemctl show -p Result -p ExecMainStatus -p NRestarts myapp.service

The systemd 257 source has messages like these. They map straight to the table above:

myapp.service: Unable to locate executable '/usr/local/bin/myap': No such file or directory
myapp.service: Changing to the requested working directory failed: No such file or directory
myapp.service: Failed to determine user credentials: No such process
myapp.service: Failed to set up mount namespacing: /var/lib/myapp: No such file or directory

Check what systemd actually loaded

A drop-in in /etc/systemd/system/myapp.service.d/ can override ExecStart=, User= or sandbox options without you noticing. systemctl cat prints the main file and every drop-in with its path. systemd-analyze verify reports unknown directives, missing dependencies and ExecStart= commands that are missing or not executable:

systemctl cat myapp.service
systemd-analyze verify myapp.service
getent passwd myapp
systemctl show -p WorkingDirectory -p User -p ProtectSystem -p ReadWritePaths myapp.service

If getent prints nothing, you have found your 217/USER. Keep in mind that systemctl cat shows files on disk. If you edited a file by hand and forgot daemon-reload, systemd may still run the old version.

Replay the start with systemd-run

To test one theory at a time, run the same binary as a transient unit with only the properties you suspect. Type=exec makes systemd report an exec failure as a start failure instead of treating the fork as success (systemd-run(1) recommends this). --wait prints the exit status at the end, and --collect unloads the transient unit even if it failed:

systemd-run --wait --collect -p Type=exec -p User=myapp /usr/local/bin/myapp --check-config
systemd-run --wait --collect -p Type=exec -p User=myapp -p ProtectSystem=strict /usr/local/bin/myapp --check-config
systemd-run --wait --collect -p Type=exec -p User=myapp -p ProtectSystem=strict -p ReadWritePaths=/var/lib/myapp /usr/local/bin/myapp --check-config

Use a config-check or dry-run flag if your program has one. Otherwise the replay starts a second copy of the service, which may fight the real one for ports or lock files. If the first command passes and the second one fails, ProtectSystem=strict is your problem. If you want to watch output interactively, swap --wait for --pty.

Sandbox failures after hardening

If you hardened a unit along the lines of sandboxing systemd services on Debian 13, expect two kinds of failure. The first is 226/NAMESPACE, which happens when a listed path is missing. systemd.exec(5) documents a - prefix in ReadWritePaths= that makes missing paths non-fatal, but in most cases it's better to create the directory or use StateDirectory=. The second is quieter: the namespace works, but ProtectSystem=strict makes everything except /dev, /proc and /sys read-only. The app then fails with 1/FAILURE and a "Read-only file system" message in its own log.

Relax one option at a time in a drop-in instead of deleting the whole hardening block. Restarting the unit interrupts the service:

systemctl edit myapp.service
[Service]
# Test only: revert once the missing path is known
ProtectSystem=full
systemctl reset-failed myapp.service
systemctl restart myapp.service
systemctl status myapp.service

systemctl edit reloads the configuration itself after you save. If the service starts with the relaxed option, put ProtectSystem=strict back and add the exact path the app needs to ReadWritePaths=.

Fixes for each cause

  • 200/CHDIR: create the directory with the right owner, or use WorkingDirectory=~ to get the home directory of User=. That needs a real home: adduser --system sets it to /nonexistent, so WorkingDirectory=~ fails with 200/CHDIR. Create the account with --home /var/lib/myapp, or use StateDirectory=myapp together with WorkingDirectory=/var/lib/myapp. A - prefix makes a missing directory non-fatal, but that only hides a deployment error.
  • Runtime files in /run: don't create /run/myapp by hand or in ExecStartPre=. /run is a tmpfs and is empty after reboot. With RuntimeDirectory=myapp, systemd creates /run/myapp on start, owned by User= and Group=, and removes it on stop.
  • 217/USER: create the account (adduser --system --group myapp) or fix the typo. For a service that needs no fixed account, DynamicUser=yes is another option.
  • 226/NAMESPACE: create the paths listed in the unit, or replace hand-made paths under /var/lib with StateDirectory=.
  • 1/FAILURE: this is your program speaking. Run its own config test and read its log. For PHP-FPM behind nginx, the nginx 502 Bad Gateway with PHP-FPM guide covers the socket and pool errors that typically show up here.

Masked units and failed dependencies

A masked unit is linked to /dev/null and cannot be started at all. systemctl status shows Loaded: masked. Check whether someone masked it on purpose before you undo it:

systemctl is-enabled myapp.service
ls -l /etc/systemd/system/myapp.service
systemctl unmask myapp.service

For "Dependency failed", the unit you started is fine. One of its Requires= dependencies, ordered with After=, failed to start, and systemd.unit(5) says the dependent unit is then not started. Find the broken one and diagnose that unit with the steps above:

systemctl --failed
systemctl list-dependencies myapp-worker.service
systemctl show -p Requires -p After myapp-worker.service

Start limits and Restart= loops

On Debian 13, systemd-system.conf(5) gives DefaultStartLimitIntervalSec=10s and DefaultStartLimitBurst=5. systemd.service(5) gives RestartSec= a default of 100ms. With Restart=on-failure, a unit that breaks during setup is started five times in well under a second and then hits the limit. Per systemd.unit(5), a unit with Restart= that reaches the start limit is not restarted any more. That's the safety net working as intended.

StartLimitIntervalSec= and StartLimitBurst= are documented as [Unit] options, while Restart= and RestartSec= belong in [Service]. Many old answers put all four in [Service]. Put them where the man page says and check the result with systemctl show:

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s
RestartSteps=4
RestartMaxDelaySec=60s
systemctl daemon-reload
systemctl show -p StartLimitBurst -p StartLimitIntervalUSec -p RestartUSec myapp.service

The wrong section fails only halfway. With both options in [Service], systemd 257 logs Unknown key 'StartLimitIntervalSec' in section [Service], ignoring and keeps the 10-second default, but it still accepts StartLimitBurst= there. systemd-analyze verify prints the warning and still exits with 0, so read its output instead of trusting the exit code.

RestartSteps= and RestartMaxDelaySec= give exponential backoff from RestartSec= up to the maximum. They only work if both are set and RestartSec= isn't zero. Watch the math, though. If the delay between restarts is longer than the interval divided by the burst, the limit is never reached and a broken unit loops forever with a growing NRestarts. Restart settings are for programs that crash now and then, not for configuration errors. A 203, 217 or 226 won't fix itself on the next try.

Prevent it next time

  • Run systemd-analyze verify on every new or changed unit before daemon-reload, ideally in your deployment pipeline.
  • Prefer RuntimeDirectory=, StateDirectory= and LogsDirectory= to directories created by hand or by scripts.
  • Add sandbox options one at a time and restart after each, so a failure points to a single change.
  • Alert on failed units (systemctl --failed in your monitoring) and on a rising NRestarts, so silent restart loops get noticed.
  • Record the exact package version you tested against: systemctl --version. At the time of writing, manpages.debian.org lists systemd 257.13-1~deb13u1 for trixie.

Takeaways

  1. Ignore "Start request repeated too quickly" and find the first status= value in the journal.
  2. Look up the code: 203 exec, 200 working directory, 217 user, 226 namespace paths, 1 the app itself.
  3. Check the loaded config with systemctl cat and systemd-analyze verify.
  4. Replay with systemd-run -p and relax one sandbox option at a time in a drop-in.
  5. Put StartLimit* in [Unit], use backoff in [Service], and don't raise limits to hide a config error.
  6. Clear the state with systemctl reset-failed after the real fix, then start again.

Sources

Comments

Latest alerts