Cron is everywhere—everyone knows it. But here’s the thing: cron is for recurring jobs. You reboot every Sunday at 2 AM, run backups daily, check logs every hour. That’s cron’s bread and butter.
But what if you need to run something exactly once, at a specific time? That’s where at comes in. It’s the Linux scheduler nobody talks about, and it’s dead simple.
Why Not Just Use Cron?
You could add a cron job that runs once and then deletes itself. You could set an alarm on your phone. Or you could use at, which exists specifically for this job.
Think of it like this: cron is your daily standup meeting. at is the one-off Zoom call your manager springs on you Friday afternoon.
Installing at
On most systems, at is already installed. Check:
$ at -Vat version 3.2.5If missing, grab it:
# Debian/Ubuntusudo apt install at
# RHEL/CentOSsudo yum install at
# Fedorasudo dnf install atThen start the service:
sudo systemctl start atdsudo systemctl enable atdBasic Syntax
at [TIME] [DATE]You pipe in your command and hit Ctrl+D when done.
$ at 3:00 PM tomorrowat> /usr/local/bin/backup.shat> <Ctrl+D>job 5 at Fri Jan 11 15:00:00 2025That’s it. One job scheduled. Time formats are forgiving:
at 10:00 AM Tuesday # Next Tuesday at 10 AMat 2:30 PM Jan 15 # Jan 15 at 2:30 PMat midnight # Tonight at 00:00at now + 2 hours # In 2 hours from nowat 14:00 + 3 days # 3 days from now at 2 PMManaging Your Queue
See what’s scheduled:
$ atq5 Fri Jan 11 15:00:00 2025 a kingpin7 Mon Jan 13 08:00:00 2025 a kingpinThe leftmost number is the job ID. Want details?
$ at -c 5#!/bin/sh# atrun uid=1000 gid=1000# mail kingpin 0umask 22HOSTNAME=laptop; export HOSTNAME.../usr/local/bin/backup.shRemove a job:
$ atrm 5$ atq7 Mon Jan 13 08:00:00 2025 a kingpinThe Batch Command
batch is at’s nerdy cousin. Instead of scheduling at a fixed time, it runs when the system load drops below 1.5:
batchat> time-consuming-analysis.pyat> <Ctrl+D>job 9 at Fri Jan 11 20:30:00 2025Perfect for heavy jobs during off-peak hours without hammering the server.
Real-World Examples
Schedule a reboot after you finish work:
at 6:00 PM todayat> sudo systemctl rebootat> <Ctrl+D>Remind yourself to check on a deploy in 15 minutes:
at now + 15 minutesat> notify-send "Check the deploy status"at> <Ctrl+D>Run a database maintenance job at 2 AM (one-time):
at 2:00 AM tomorrowat> /opt/scripts/db-cleanup.sh >> /var/log/db-cleanup.log 2>&1at> <Ctrl+D>Stagger a series of deploys across fleet:
for server in web1 web2 web3; do at now + $((RANDOM % 60)) minutesat> ssh $server 'sudo systemctl restart myapp' at> <Ctrl+D>doneGotchas
Jobs don’t have your shell aliases. Use full paths to scripts and binaries.
No email by default. If you want output mailed to you, pipe to | mail -s "Job done" user@host inside the at script.
atd must be running. If systemd isn’t managing it, jobs won’t fire.
Permissions. Check /etc/at.allow and /etc/at.deny. If either exists, only listed users can use at.
Using at with a Script File
Instead of interactive mode, pipe a script in directly:
echo '/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1' | at 3:00 AM tomorrowOr with a heredoc for multi-line:
at 6:00 PM today << 'EOF'cd /var/www/myappgit pull origin mainsystemctl restart myappecho "Deploy done" | mail -s "Deploy status" admin@example.comEOFClean, no interactive prompt, scriptable.
Environment Variables
at doesn’t inherit your current shell environment. It captures the environment at the time you run at and replays it. But PATH is often limited. Safe pattern:
at now + 5 minutes << 'EOF'export PATH=/usr/local/bin:/usr/bin:/bin/usr/local/bin/my-script.shEOFAlways use absolute paths inside at jobs. Assume nothing is in PATH.
Checking atd Logs
If your job didn’t run, check the daemon:
sudo journalctl -u atd -n 50Common reasons jobs fail: atd wasn’t running, permissions in /etc/at.allow, or the script path was wrong.
When to Reach For at
- One-off tasks (reboot, backup, cleanup)
- Scheduled delays (deploy in 5 minutes)
- Load-aware jobs (
batch) - Anything cron would overkill for
It’s humble. It’s reliable. And it does exactly one thing well. Your 2 AM self will be grateful when you realize you didn’t need to write a self-deleting cron job.