20.5 at: One-Time Scheduled Jobs
Alright, let’s talk about at. If cron is your meticulous, obsessive calendar for tasks that happen over and over, at is its free-spirited, slightly scatterbrained cousin who you call to do one thing, one time, in the future. “Hey, reboot the server at 2 AM,” or “Download that huge file at 3 PM when the network’s quiet.” It’s a brilliantly simple tool that, frankly, doesn’t get enough love.
The core concept is brain-dead simple: you tell at when to run a command, then you feed it what to run. It’s not a daemon that’s always running like crond; it’s a utility that schedules a job with the atd daemon, which then forks off a child process to run your command at the appointed time. It’s a fire-and-forget missile for your command line.
How to Speak ‘at’
The syntax is wonderfully straightforward. You specify the time, and then you’re dropped into a prompt to type the commands you want to run. You can also pipe commands into it, which is my preferred method because it keeps everything in my shell history.
Let’s say you want to shut down your desktop machine at midnight because you’re downloading the entire Linux kernel source tree and you want to make sure it doesn’t run all weekend.
echo "shutdown -h now" | at midnight
Boom. Done. The at command understands a ton of human-friendly time formats. midnight, noon, teatime (seriously, it’s 4 PM), tomorrow, next week, or specific times like 11:00 PM, 23:00, 11:00 PM Oct 20, or even relative times like now + 2 hours or now + 3 days.
Here’s a more complex example. Let’s say it’s 10 AM and you want to run a backup script at 10 PM tonight and have the output emailed to you (more on that later).
echo "/home/user/scripts/my_backup.sh" | at 10pm
Checking Your Work (atq) and Changing Your Mind (atrm)
So you’ve fired off a few at jobs. How do you see what’s queued up? You use atq (short for “at queue”).
$ atq
15 Fri Sep 15 22:00:00 2023 a user
12 Sat Sep 16 09:00:00 2023 a user
The first number is the job number. You’ll need that number if you get cold feet and want to delete a job using atrm (short for “at remove”).
atrm 15
And just like that, job number 15 is vaporized. No confirmation, no fanfare. It’s just gone.
Where the Magic (and the Problems) Happen
This is the critical part most tutorials gloss over. When your at job runs, it does not run in your current, cozy shell environment. It runs in a bare-bones, almost pristine environment. This is the number one reason at jobs fail silently.
Think about it: your fancy PATH variable, your AWS_ACCESS_KEY_ID environment variable, your current working directory—none of that is there. The at job runs with a very basic environment, usually just what’s defined in /etc/environment and a few basic defaults.
This is why you must use absolute paths to commands and scripts. Your python3 script won’t run if you just call python3 myscript.py because python3 might not be in the default PATH. You need to use /usr/bin/python3.
Bad:
echo "python3 /home/user/scripts/cleanup.py" | at now + 1 hour
Good:
echo "/usr/bin/python3 /home/user/scripts/cleanup.py" | at now + 1 hour
Similarly, if your script assumes it’s running from a certain directory, you need to cd into it first.
echo "cd /home/user/project && /usr/bin/bash ./build.sh" | at now + 5 minutes
The Almighty Output Problem
Here’s another classic at pitfall. What happens to the output (stdout/stderr) of your command? It doesn’t just magically appear on your terminal hours later. By default, at will email it to you locally using the system’s mailer (like /usr/bin/mail). If you’re like 99.9% of us, you’ve never set up local mail and have no idea how to read it. So the output of your job is probably just… lost in the ether.
You have two solid solutions:
Redirect it to a file. This is the simplest and most reliable method.
echo "/usr/bin/python3 /home/user/script.py > /home/user/at_logs/script.log 2>&1" | at midnightUse the
-moption. This tellsatto send you an email even if the command produces no output. This requires your local mail system to be at least minimally functional enough to deliver to your user’s local mailbox (usually/var/mail/username). You can check this withmail.echo "/usr/bin/python3 /home/user/script.py" | at -m midnight
Access Control: Who Gets to ‘at’?
This isn’t just a best practice; it’s a security necessity. The files /etc/at.allow and /etc/at.deny control who can use at. The rules are simple:
- If
at.allowexists, only users listed in it can useat. - If
at.allowdoesn’t exist, butat.denydoes, everyone except users inat.denycan useat. - If neither file exists, only
rootcan useat.
On most modern systems, you’ll find an empty at.deny file, meaning everyone can use it. On a multi-user system, you should absolutely revisit this. Letting any user schedule a job running as themselves is a potential resource hog waiting to happen.
So, there you have it. at: incredibly useful, deceptively simple, and just waiting to bite you if you ignore its quirks about environment and output. Use it. Love it. But for goodness sake, use absolute paths and redirect your output. I don’t want to get an email from your future self asking why the script didn’t run.