Cron is for recurring jobs. When you need something to run once later (restart a service at 2 a.m., revert a firewall change in 10 minutes if you get locked out, send a reminder), use at. It queues the command, and runs it at the given time, even after you log out.
Install and enable
sudo apt install at # Debian/Ubuntu
sudo dnf install at # RHEL/AlmaLinux
sudo systemctl enable --now atd
Schedule a job
Type commands at the at> prompt, then press Ctrl+D:
at 02:00
at> systemctl restart php8.3-fpm
at> <Ctrl+D>
job 3 at Thu Nov 6 02:00:00 2026
Or pipe the command in (better for scripts):
echo "systemctl restart php8.3-fpm" | sudo at 02:00
echo "/usr/local/bin/report.sh" | at now + 2 hours
at -f /usr/local/bin/cleanup.sh 23:30
Time formats
| Expression | Meaning |
|---|---|
now + 10 minutes |
10 minutes from now |
now + 2 hours, now + 3 days |
Relative times |
14:30, 2:30 PM |
Today (or tomorrow if already past) |
midnight, noon, teatime |
00:00, 12:00, 16:00 |
02:00 tomorrow |
Tomorrow at 2 a.m. |
10:00 2026-12-24 |
Specific date |
next monday |
Next Monday, at the current time |
Times use the server’s time zone. Check with timedatectl. See timedatectl and time zones.
List, inspect and remove jobs
atq # list pending jobs (same as at -l)
at -c 3 # show the full script of job 3 (including environment)
atrm 3 # remove job 3
Root sees all users’ jobs. Normal users see their own.
The lockout safety net
A classic sysadmin trick before risky remote changes (firewall rules, SSH config, network settings): schedule an automatic undo, make the change, and cancel the undo only if you can still connect:
echo "ufw disable" | sudo at now + 10 minutes
sudo ufw enable
# open a NEW SSH session to confirm access, then:
sudo atq
sudo atrm <job-number>
If the change locks you out, the server reverts it in 10 minutes. For network changes on Ubuntu, netplan try has the same idea built in. And a VNC console in your VPS control panel is the final fallback. See UFW and VPS VNC console.
batch: run when the system is idle
batch queues a job to run as soon as the load average drops below a threshold (1.5 by default):
echo "nice -n 19 /usr/local/bin/reindex.sh" | batch
That’s handy for heavy one-off tasks on a busy server. See uptime and load average.
Output and environment
- Output (stdout/stderr) is mailed to the user, if local mail works. Otherwise redirect it:
echo "/path/job.sh >> /var/log/job.log 2>&1" | at 03:00. - Unlike cron,
atsaves your current environment (working directory, variables) with the job, so commands usually behave as they would in your shell. Use absolute paths anyway. - The shell is
/bin/sh.
Permissions
Access is controlled by /etc/at.allow and /etc/at.deny (like cron). If at.allow exists, only users listed in it may use at.
Alternative: systemd-run
On systemd systems, transient timers work without atd:
sudo systemd-run --on-active=10m systemctl restart nginx
sudo systemd-run --on-calendar="2026-11-06 02:00" /usr/local/bin/report.sh
systemctl list-timers
Output goes to the journal. See systemd timers.
Quick reference
| Task | Command |
|---|---|
| Run later | echo "cmd" | at now + 10 minutes |
| From a file | at -f script.sh 02:00 |
| List | atq |
| Show job | at -c <id> |
| Remove | atrm <id> |
| When idle | echo "cmd" | batch |
Related: crontab guide and anacron.