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:

  1. 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 midnight
    
  2. Use the -m option. This tells at to 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 with mail.

    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.allow exists, only users listed in it can use at.
  • If at.allow doesn’t exist, but at.deny does, everyone except users in at.deny can use at.
  • If neither file exists, only root can use at.

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.