You Already Have a Job Scheduler, a Process Babysitter, and a Cage
You need to run a long script and keep it alive after you close the terminal. So you type nohup ./job.sh &, close the laptop, and hope. Or you open tmux. Or you write a 12-line unit file, drop it in /etc/systemd/system/, run daemon-reload, and then forget to delete it.
systemd-run does all of that in one command. It creates a transient unit (a service, scope, or timer that exists only in memory and under /run), starts it, and lets systemd handle logging, limits, and cleanup. No file to write. No file to forget.
My opinion: for anything you’d call “one-off” or “let me just try this,” systemd-run is a better tool than nohup, screen, at, and ulimit on any box that runs systemd. You pay for it with a few extra characters of typing. Everything below was run on systemd 262, with --user so nothing needed root.
The Old Habits, Side by Side
| You want | Old habit | systemd-run |
|---|---|---|
| Keep it running after logout | nohup cmd &, tmux, screen | systemd-run --user cmd |
| Run it later | at now + 1 hour | systemd-run --on-active=1h cmd |
| Run it on a calendar schedule | a crontab entry you forget | systemd-run --on-calendar=... cmd |
| Cap memory or CPU | ulimit, cgexec, nice | -p MemoryMax=... -p CPUQuota=... |
| Lock it down | hand-rolled unshare or bubblewrap | -p ProtectSystem=strict ... |
| Find the output later | nohup.out, hope | journalctl -u name |
The old tools each solve one row. systemd-run covers all of them with the same syntax, and the logs land in the journal where you already look.
A Transient Service in One Line
$ systemd-run --user --unit=backup-test sleep 60Running as unit: backup-test.service; invocation ID: 55aa86f8777f488bb0021d5f52f0de88That’s it. The process is now a real service. Check on it like any other:
$ systemctl --user status backup-test● backup-test.service - [systemd-run] /usr/bin/sleep 60 Loaded: loaded (/run/user/1000/systemd/transient/backup-test.service; transient) Transient: yes Active: active (running) since Tue 2026-11-03 15:01:13 EST; 6ms ago Main PID: 687472 ((sleep))Note the Transient: yes line and the path under /run. That directory is a tmpfs, so a reboot erases the unit. You never wrote it, and you never have to clean it up.
If you skip --unit=, systemd invents a name like run-p687462-i9066693.service. Fine for throwaway jobs. Give it a name when you want to find it again with journalctl -u.
Stopping it is also the normal command:
$ systemctl --user stop backup-test$ journalctl --user -u backup-testAfter a stop, the unit is gone. systemctl --user status backup-test now says Unit backup-test.service could not be found.
System Mode vs --user Mode
Without --user, systemd-run talks to the system manager and needs root (or polkit approval). With --user, it talks to your own user manager. No privileges, and the unit runs as you.
Use --user by default. It’s the forklift-versus-pallet-jack choice: you rarely need the forklift, and the pallet jack doesn’t need a license.
User units have a shelf life. They normally die when your last login session ends. If a --user unit must outlive logout (a long download on a server you SSH into, say), turn on lingering once:
$ loginctl enable-lingerPer the man page, users can enable lingering for themselves in the default configuration. With lingering on, your user manager starts at boot and stays up after you log out, which is what screen people were faking all along.
Services vs Scopes: Who Owns the Process?
A service (the default) is started by systemd. systemd forks the command, holds it, and systemd-run returns immediately. The process is not a child of your shell.
A scope (--scope) is different. Your command starts as a child of systemd-run, in the foreground, and systemd only wraps it in a cgroup so it can track and limit it.
$ systemd-run --user --scope -p MemoryMax=50M echo inscopeRunning as unit: run-p687532-i9093782.scope; invocation ID: b259d839624d4248b532e6cfb9a7f3c4inscopePick by what you need:
- Service: the job should run detached, survive your terminal, have its own journal entries, and be restartable by name. Background work, long jobs, anything you walk away from.
- Scope: you want to stay attached. Your shell, your stdin, your stdout, your Ctrl-C, but with cgroup limits. A compile you want to watch, with a memory ceiling.
Scopes have one limit worth knowing. They can’t be combined with timer options. Try it and you get Path, socket or timer options are not supported in --scope mode. If you want a delay, you need a service.
Resource Limits Without cgexec
This is where systemd-run replaces ulimit, nice, and cgexec outright. Pass any resource-control property with -p:
$ systemd-run --user --unit=reindex \ -p MemoryMax=2G \ -p CPUQuota=50% \ -p IOWeight=50 \ -p TasksMax=200 \ ./reindex.shWhat those do (per systemd.resource-control):
MemoryMax=is a hard ceiling. Go over it and the kernel OOM killer acts inside that unit’s cgroup.MemoryHigh=is the softer cousin: it throttles instead of kills.CPUQuota=50%caps the unit at half of one CPU’s time.200%means two full CPUs.IOWeight=sets the relative disk share against other units in the same slice. The kernel only enforces it with a weight-aware I/O controller such as BFQ or io.cost.TasksMax=limits how many processes and threads the unit can spawn. A cheap fork-bomb fence.
The swap gotcha
I tested MemoryMax=30M with a Python script that allocates 200 MB. The script survived. The unit report said Memory peak: 30M (swap: 172M). The kernel pushed the excess into swap instead of killing anything.
If you want the kill, cap swap as well:
$ systemd-run --user --wait --pipe -p MemoryMax=30M -p MemorySwapMax=0 \ python3 -c 'x=bytearray(200*1024*1024); print("survived")'Running as unit: run-p688430-i9079379.service Finished with result: oom-killMain processes terminated with: code=killed, status=9/KILLNow the limit means what you thought it meant. Your 2 AM self will appreciate knowing this before the swap partition fills.
Changing limits on a running unit
You don’t have to stop and restart. systemctl set-property works on transient units:
$ systemd-run --user --unit=big-job -p CPUQuota=50% sleep 300$ systemctl --user set-property big-job CPUQuota=25%$ systemctl --user show big-job -p CPUQuotaPerSecUSecCPUQuotaPerSecUSec=250msThat’s renice and cpulimit for people who read the manual.
Test Sandboxing Before It Goes in a Unit File
Hardening a service is mostly guesswork: you add ProtectSystem=strict, restart, and find out at 2 AM that the app needed to write somewhere. systemd-run lets you try the property on the actual command first.
$ systemd-run --user --wait --pipe \ -p ProtectSystem=strict -p ReadWritePaths=/tmp \ sh -c 'touch /etc/x; touch /tmp/ok && echo tmpok'Running as unit: run-p687544-i9083464.servicetouch: cannot touch '/etc/x': Read-only file systemtmpokProtectSystem=strict makes the whole filesystem read-only except what you list in ReadWritePaths=. The /etc write failed, the /tmp write worked. The same trick with home directories:
$ systemd-run --user --wait --pipe \ -p PrivateTmp=yes -p ProtectHome=read-only -p NoNewPrivileges=yes \ sh -c 'touch ~/x; ls /tmp | wc -l'touch: cannot touch '/home/alice/x': Read-only file system0Here PrivateTmp=yes gave the process its own empty /tmp (the 0), ProtectHome=read-only blocked the write, and NoNewPrivileges=yes stops the process from gaining privileges through setuid binaries. Property names are the same ones documented in systemd.exec.
The workflow: run your real command with a candidate property, watch what breaks, loosen exactly that, then copy the working set into a unit file. Iterating with -p takes seconds. Iterating through daemon-reload and restarts takes minutes, and you’ll skip it.
Transient Timers: at and Cron’s Cleaner Cousin
Add a timer option and systemd-run creates a transient .timer plus the service it triggers:
$ systemd-run --user --unit=nightly-note --on-active=5min echo hiRunning timer as unit: nightly-note.timerWill run service as unit: nightly-note.serviceCheck the queue with the same tool you use for real timers:
$ systemctl --user list-timersNEXT LEFT LAST PASSED UNIT ACTIVATESTue 2026-11-03 15:06:14 EST 4min 59s - - nightly-note.timer nightly-note.serviceThe timer flags, which the man page describes as shortcuts for --timer-property= with the matching systemd.timer settings:
--on-active=runs the command that long after you ransystemd-run. This isat now + 5 minutes.--on-boot=and--on-startup=count from boot (system manager) or from user manager start.--on-unit-active=repeats relative to the last time the service ran. Pair it with--on-active=for a simple “start in 10 seconds, then every hour”:
$ systemd-run --user --unit=poll --on-active=10 --on-unit-active=1h ./check.sh--on-calendar=takes a calendar expression, same syntax asOnCalendar=insystemd.timer:
$ systemd-run --user --unit=quarter-hour --on-calendar='*:0/15' ./sync.shTo cancel any of them, stop the timer, not the service:
$ systemctl --user stop poll.timerCompared to at, you get the journal, resource limits via -p, and a unit you can list and cancel by name. Compared to cron, you get no crontab edit and no forgetting. Remember that transient timers vanish at reboot. If a schedule must survive reboots, write the real timer unit.
Waiting, Piping, and Interactive Runs
By default systemd-run returns the moment the unit starts. Four flags change that.
--wait blocks until the unit finishes and prints a summary: result, exit status, runtime, CPU time, memory peak. It’s the one to use in scripts:
$ systemd-run --user --wait -p MemoryMax=64M -p CPUQuota=20% ./job.shRunning as unit: run-p1234-i5678.service Finished with result: successMain processes terminated with: code=exited, status=0/SUCCESS Service runtime: 13ms CPU time consumed: 5ms Memory peak: 2.2M (swap: 0B)--pipe (-P) wires the service’s stdin, stdout, and stderr to your shell’s pipes, so the command works in a pipeline. Combine --wait --pipe and a transient service behaves like a normal command with a cgroup around it.
--pty (-t) connects the unit to your terminal through a pseudo-TTY, for things that expect interactive input. A shell, for instance:
$ systemd-run --user -t -p MemoryMax=500M bashEverything you run in that shell lives under the 500M ceiling.
--no-block goes the other way: don’t even wait for the start job to finish.
Environment and working directory
A transient service does not inherit your shell’s directory or environment. Two flags fix that:
$ cd /srv/backup$ systemd-run --user --wait --pipe --same-dir -E FOO=bar sh -c 'pwd; echo $FOO'/srv/backupbar--same-dir (-d) uses your current directory. --working-directory=/some/path sets one explicitly. -E NAME=VALUE (--setenv) sets a variable. Leave off =VALUE and the variable is copied from your current environment.
What Happens After the Command Exits
A transient service that finishes successfully is unloaded right away. One that fails stays in memory, marked failed, until you clear it:
$ systemd-run --user --unit=flaky false$ systemctl --user list-units --failed UNIT LOAD ACTIVE SUB DESCRIPTION● flaky.service loaded failed failed [systemd-run] /usr/bin/false$ systemctl --user reset-failed flakyThat’s deliberate, since a failure is evidence you might want to read in the journal. But run a flaky job in a loop and you’ll fill list-units --failed with junk.
--collect (-G) tells systemd to unload the unit after it finishes, even on failure:
$ systemd-run --user --wait --collect --unit=flaky2 false Finished with result: exit-code$ systemctl --user status flaky2Unit flaky2.service could not be found.Use --collect for fire-and-forget jobs. Skip it when you want to inspect a failure afterward.
--remain-after-exit does the opposite: keep the unit around after the process exits, so you can still query its status and accounting. It can’t be combined with --wait. Try it and systemd-run answers --wait may not be combined with --remain-after-exit.
The journal entries outlive the unit either way, so journalctl --user -u reindex still works after a clean exit, as long as the journal itself keeps those logs.
-p vs the Dedicated Flags
systemd-run has shortcut flags for the common properties: --uid=, --gid=, --nice=, --slice=, --working-directory=, --setenv=. Each is equivalent to the matching property (--nice= is Nice= from systemd.exec, for example).
Use the dedicated flag where one exists, and -p (long form --property=) for everything else. Resource limits and sandboxing have no shortcut flags, so they’re always -p. You can repeat -p as often as you like.
For timers, --timer-property= passes settings to the timer unit instead of the service. The --on-* flags already cover the common ones.
run0: The Sudo Replacement Built on This
systemd 256 added run0. Per the NEWS file, systemd-run became a multi-call binary, and when invoked as run0 it runs a command as another user (root by default) through the same transient-unit machinery. There’s no setuid binary involved. It’s a different topic with a different set of trade-offs, so treat this as a pointer, not a tutorial.
What to Use When
- Background job that must survive a logout:
systemd-run --user --unit=name cmd, plusloginctl enable-lingerif the machine has no active session. - Foreground command with a memory or CPU cap:
systemd-run --user --scope -p MemoryMax=... cmd. Add-p MemorySwapMax=0if you want a real kill. - One-off delayed run:
--on-active=. Recurring: add--on-unit-active=. - Testing a hardening property:
--wait --pipe -p ProtectSystem=strict ...until it stops breaking, then write the unit file. - Script that needs the exit code and stats:
--wait --collect. - Anything permanent: write a real unit file. Transient means gone at reboot, and that’s the feature.
You don’t need a unit file for every idea. You just need one for the ideas that last.
Common Questions
Does systemd-run work without root?
Yes. Running systemd-run --user talks to your own user manager, so the unit runs as you and needs no privileges. Without --user, systemd-run targets the system manager and needs root or polkit authorization. Resource limits and sandbox properties work in user mode on systemd 262.
Do transient units survive a reboot?
No. Transient units live under /run, which is a tmpfs, so a reboot erases them along with their timers. For a schedule or service that must come back after boot, write a normal unit file and enable it. Journal logs may persist, depending on your journald storage setting.
Does systemd-run work inside a Docker container?
No, not in a standard Docker container. systemd-run needs a running systemd manager to talk to over D-Bus, and a typical container runs the application itself as PID 1 with no systemd at all. Containers that boot systemd as PID 1, such as systemd-nspawn machines, do support systemd-run.
Why did MemoryMax not kill my process?
Because the kernel can push the excess memory into swap instead of killing the process. In one test, a script that allocated 200 MB under MemoryMax=30M survived with 172 MB in swap. Add -p MemorySwapMax=0 to the command and the unit gets OOM-killed as expected.