cron gets all the attention, but it’s the wrong tool for a huge category of everyday sysadmin needs: “run this one thing, once, later tonight.” That’s exactly what at is for. I reach for it constantly for one-off maintenance windows, delayed reboots, and “remind me in an hour” style scripts — it’s one of the most underrated commands in the standard Linux toolkit.
What at Actually Is
at schedules a command (or a whole script) to run once, at a specific point in the future, and then exits — no recurring schedule, no crontab entry to remember to clean up. It’s part of the at package, which also includes the related commands atq (queue listing) and atrm (job removal), plus the background daemon atd that actually executes the jobs when their time comes.
Where cron is built for “every day at 2 AM forever,” at is built for “in 45 minutes, do this one thing.”
Basic Syntax
at [options] TIME
atq
atrm job_id [job_id ...]
You don’t put the command on the same line as at — instead, at drops you into an interactive prompt (or reads from stdin) where you type the command(s) to run, then finish with Ctrl+D.
A Basic Example
at 22:00
warning: commands will be executed using /bin/sh
at> /usr/local/bin/backup.sh
at> <EOT>
job 3 at Fri Jul 31 22:00:00 2026
You type the command(s), press Enter, then press Ctrl+D to signal end-of-input. at confirms the job number and scheduled time.
For scripting, it’s usually cleaner to pipe the command in instead of typing interactively:
echo "/usr/local/bin/backup.sh" | at 22:00
Or schedule a whole script file with -f:
at -f /usr/local/bin/backup.sh 22:00
Time Specification Formats
at accepts a genuinely flexible, almost English-like time syntax:
at 10:00pm
at 22:00
at now + 1 hour
at now + 30 minutes
at noon
at midnight
at teatime # 4pm, a fun historical quirk
at 10:00 tomorrow
at 3:00pm next week
at 2026-08-15 09:00
at 09:00 Aug 15
Some useful patterns:
at now + 1 day # exactly 24 hours from now
at now + 2 weeks
at 5pm + 3 days # 5pm, three days from now
Full Option Reference
-f file read commands from file instead of stdin
-m send email to the user when the job completes, even with no output
-M never send email
-v show the scheduled time before reading the job
-c job_id display the actual commands the given job will run
-l same as atq (list pending jobs)
-d job_id same as atrm (delete a job)
-q queue use a specific queue letter (a–z, A–Z); default queue is 'a'
-t time specify the time in [[CC]YY]MMDDhhmm[.ss] format
-V print version
Using -f for Script Files
at -f /usr/local/bin/generate_report.sh now + 2 hours
This is the pattern I use most in scripting/automation contexts — reference an existing, tested script rather than typing commands inline.
Checking What a Job Will Actually Run
at -c 3
This dumps the full environment and command that job number 3 will execute — extremely useful for debugging a scheduled job that isn’t producing the output you expect, since it shows you exactly what environment variables were captured at scheduling time.
Queues
at supports multiple queues, denoted by single letters. The default queue is a; the batch queue (see below) is b. Jobs in “higher” alphabetical queues run with increasingly lower priority (higher nice value). This is a rarely-used feature, but useful for separating “must run promptly” jobs from “run when the system is idle” jobs:
at -q c 23:00
Listing Pending Jobs: atq
atq
3 Fri Jul 31 22:00:00 2026 a john
4 Sat Aug 1 09:00:00 2026 a john
The columns are: job number, scheduled date/time, queue letter, and owning user. As a regular (non-root) user, you only see your own jobs; root sees everyone’s.
Removing Scheduled Jobs: atrm
atrm 3
Removes job 3 from the queue silently (no output on success). You can remove multiple jobs at once:
atrm 3 4 5
Or combine listing and removal in a script — for example, to clear every job scheduled by the current user:
atq | awk '{print $1}' | xargs -r atrm
The batch Command
Closely related to at is batch, which schedules a command to run as soon as system load drops below a threshold (defined by atd, default around 1.5, adjustable with atd -l), rather than at a fixed clock time:
batch
at> /usr/local/bin/heavy_compile.sh
at> <EOT>
This is genuinely useful for resource-intensive, non-urgent jobs on shared or load-sensitive systems.
Where at Jobs Actually Live
Pending jobs are stored as individual spool files, typically in:
/var/spool/cron/atjobs/ # modern systems
/var/spool/at/ # older systems
Each spool file is essentially a self-contained shell script capturing the environment (working directory, environment variables, umask) at the moment you scheduled it — which is why a job scheduled from an interactive SSH session with certain env vars set will run later with that environment, not whatever the environment looks like at execution time.
How It Works Internally
The atd daemon runs continuously in the background (check with systemctl status atd), waking periodically to check the spool directory for jobs whose scheduled time has arrived. When a job’s time comes, atd executes it via /bin/sh, capturing stdout and stderr, and — unless -M was used — emails any output to the owning user through the local mail system, exactly like cron does.
Controlling Access
Just like cron, access to at is gated by two optional files, checked in order:
/etc/at.allow— if present, only listed users may useat./etc/at.deny— ifat.allowdoesn’t exist, users listed here are blocked; everyone else allowed.
sudo cat /etc/at.allow /etc/at.deny 2>/dev/null
Real-World Examples
Scheduling a maintenance-window reboot:
echo "/sbin/shutdown -r now" | at 03:00
Delayed cleanup after a deployment:
at now + 30 minutes <<'EOF'
rm -rf /tmp/deploy_staging_*
systemctl restart myapp
EOF
Warning users before a scheduled maintenance shutdown:
echo "wall 'System going down for maintenance in 5 minutes'" | at now + 55 minutes
echo "shutdown -h now" | at now + 60 minutes
A cleanup script that self-schedules its own removal reminder:
#!/bin/bash
# deploy.sh
./run_deployment.sh
echo "/usr/local/bin/verify_deployment.sh" | at now + 10 minutes
Auditing what’s currently pending across all users (as root):
for job in $(atq | awk '{print $1}'); do
echo "=== Job $job ==="
at -c "$job" | tail -5
done
Troubleshooting
“at: command not found” — the at package isn’t installed. Install with apt install at (Debian/Ubuntu) or dnf install at (RHEL/Fedora).
Job doesn’t run at the scheduled time — check atd is running: systemctl status atd; enable it if needed with systemctl enable --now atd.
Job ran but had no effect / wrong environment — remember the job’s environment (PATH, working directory, env vars) is captured at scheduling time, not execution time. If you scheduled it from a stripped-down environment (e.g., via a script called by cron), it inherits that stripped environment. Use at -c job_id to inspect exactly what’s stored.
No output visible after job runs — same as cron, output goes to local mail unless redirected; check mail for the owning user, or explicitly redirect inside the job:
echo "/usr/local/bin/task.sh >> /var/log/task.log 2>&1" | at 22:00
Permission denied using at — check /etc/at.allow and /etc/at.deny.
Comparison to Related Tools
cron/crontab— for recurring schedules;atis strictly for one-time execution.systemd-run --on-calendar=/ transient timers — the systemd-native equivalent, capable of one-shot scheduled execution with full journal logging and dependency management; more powerful but more verbose thanat.sleep && command— a crude manual alternative (sleep 3600 && ./task.sh &), but it ties the job to your current shell session and is lost if the session ends, unlikeat, which persists independent of your login session.batch— load-based deferred execution, part of the same package.
at behaves consistently across Debian, Ubuntu, RHEL, CentOS, Fedora, and SUSE, since it’s essentially always the same upstream at package. The main variance is whether it’s installed by default (increasingly, it isn’t, and needs a manual apt/dnf install at).
Summary
at fills the exact gap that cron leaves open: quick, one-off, future-scheduled commands that don’t deserve a permanent crontab entry. Combined with atq for visibility and atrm for cleanup, it’s a lightweight, dependable trio for anything from “reboot in 20 minutes” to “run this report once, tonight.” Learn the flexible time syntax once, and you’ll find yourself reaching for it far more often than you’d expect.
References
man at,man atq,man atrm,man atdon your local system- GNU/Linux
atpackage documentation (Debian and Red Hat package repositories) - Debian Administrator’s Handbook, “Task Scheduling: cron and atd”
