A Type=oneshot service represents finite work whose startup completes when its start commands finish successfully. That makes it useful for setup steps and scheduled jobs whose completion matters to a dependent unit. The word oneshot describes one activation’s process model. It does not create a permanent record that prevents the task from running again after a restart, another activation, or a reboot.
When it breaks
A successful oneshot normally becomes inactive when its commands finish. That is expected behavior for a completed task, not evidence that a daemon crashed. With RemainAfterExit=yes, the manager instead keeps the unit active after the commands exit. An active (exited) state can therefore have no main process at all. Monitoring that assumes every active service needs a live PID will misclassify this service type.
Multiple ExecStart entries run in sequence for a oneshot. Under ordinary success rules, a failing command prevents later start commands from running and marks the activation failed. Earlier commands may already have changed files or databases. systemd does not roll those effects back, so retrying the activation can repeat work that succeeded before the failure.
RemainAfterExit is manager state, not a durable exactly-once guarantee. Starting an already active unit does not rerun it, but restarting it does. A timer targeting an active oneshot also does not turn each elapsed interval into another execution. Choose the retained state only when it represents a real ongoing condition, and keep repeatable scheduled work inactive between runs.
Check it quickly
Run this Python script as an administrator in a disposable Linux environment managed by systemd. It creates one runtime unit and a temporary step program. The recorded run used Debian 12 with systemd 252.39. Ensure the example unit filename is unused. Each step records its name before the second step deliberately fails:
from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
unit='xbcatm-oneshot-lab.service';path=Path('/run/systemd/system')/unit
assert not path.exists()
def ctl(*args,success=True):
result=subprocess.run(['systemctl',*args],text=True,capture_output=True,timeout=10)
assert (result.returncode==0)==success,(args,result.stderr)
return result.stdout.strip()
def value(key):return ctl('show',unit,'-p',key,'--value')
with TemporaryDirectory(prefix='xbcatm-oneshot-',dir='/run') as directory:
root=Path(directory);worker=root/'step.py';calls=root/'calls'
worker.write_text("""from pathlib import Path
import sys
root=Path(__file__).parent
with (root/'calls').open('a') as output: output.write(sys.argv[1]+'\\n')
if sys.argv[1]=='second' and not (root/'healthy').exists(): sys.exit(7)
""")
definition=f'''[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=3s
ExecStart=/usr/bin/python3 {worker} first
ExecStart=/usr/bin/python3 {worker} second
ExecStart=/usr/bin/python3 {worker} third
'''
path.write_text(definition)
try:
subprocess.run(['systemd-analyze','verify',str(path)],check=True)
ctl('daemon-reload');ctl('start',unit,success=False)
assert calls.read_text().splitlines()==['first','second']
assert value('Result')=='exit-code' and value('ExecMainStatus')=='7'
print('second command fails; third command does not run')
(root/'healthy').touch();ctl('reset-failed',unit);ctl('start',unit)
assert calls.read_text().splitlines()==['first','second','first','second','third']
assert (value('ActiveState'),value('SubState'),value('MainPID'))==('active','exited','0')
before=calls.read_text();ctl('start',unit);assert calls.read_text()==before
print('active/exited with PID 0; repeated start does not rerun')
ctl('restart',unit)
assert calls.read_text()==before+'first\nsecond\nthird\n'
print('restart executes all commands again')
ctl('stop',unit);path.write_text(definition.replace('RemainAfterExit=yes','RemainAfterExit=no'))
ctl('daemon-reload');before=calls.read_text()
for _ in range(2):
ctl('start',unit)
assert value('ActiveState')=='inactive' and value('Result')=='success'
assert calls.read_text()==before+'first\nsecond\nthird\n'*2
print('without RemainAfterExit: two starts, two successful executions')
finally:
ctl('stop',unit)
if value('ActiveState')=='failed':ctl('reset-failed',unit)
path.unlink();ctl('daemon-reload')
The initial activation records only first and second, returns a failing start command, and exposes exit status seven. The third command does not run. The existing log entries remain, which makes the partial effects visible instead of pretending that a failed activation left no work behind.
After the healthy marker is created, the retry records all three steps. The complete file now contains the two earlier entries followed by a fresh sequence of three. State becomes active and exited, with main PID zero. That combination means the manager retains a successful activation, not that the program remains running in the background.
A second start while active leaves the file unchanged. Restarting the same unit appends another complete sequence. This is the direct counterexample to interpreting oneshot as “execute this migration once for the lifetime of the machine.” The manager is doing exactly what the new activation requests.
The script then stops the unit, changes RemainAfterExit to no, reloads the manager, and starts it twice. Both executions succeed and both end inactive. The log gains two full sequences. This behavior fits a job that should run whenever triggered and complete before another trigger can start it again.
The unit has an explicit three-second startup timeout. Oneshot startup timeouts are disabled by default in the documented version, so a stuck command otherwise may hold an activation open indefinitely. Choose a realistic bound for actual work rather than copying this small laboratory timeout into a long migration.
Give the task its own correctness guarantees
Keep each start command finite and in the foreground. If the task launches a long-running worker, describe that worker with an appropriate service lifecycle instead of inferring continued operation from the parent’s successful exit. Unit type, command completion, and the business result should describe the same event.
For a migration, use the database’s migration ledger, transaction behavior, and concurrency controls to decide whether a change has already been applied. A retained active state in one manager cannot coordinate multiple hosts or survive every operational transition. Make retries safe at the layer where side effects occur, and preserve the result needed to diagnose partial completion.
Inspect the service’s recorded result and application output, not just whether a timer or dependency triggered it. A timer can be active while its last job failed. Separate installation, scheduling, and execution guarantees, and verify a failed path as well as the successful one before depending on the task during boot or release.
Further reading
Sources
- systemd 252 service types, oneshot command ordering, retained state, and startup timeouts, checked 2026-09-12.
- systemd 252 systemctl, start, restart, and manager reload behavior, checked 2026-09-12.
- systemd 252 timers, activation of an already active target service, checked 2026-09-12.