Skip to content
shellhacks.dev
Cron and Scheduling

How to Use At for One Time Jobs and atq the Right Way

Schedule one time jobs with at: time formats, reading from a file, listing with atq, removing with atrm, batch for idle time and the environment it runs in.

6 min read
Summarize in AI
Illustration for "How to Use At for One Time Jobs and atq the Right Way": a shell script in an editor, a loop arrow, progress bars.

Cron is for things that repeat. A single future action, such as restarting a service after hours, running a long job tonight, or removing a temporary firewall rule in 30 minutes, does not belong in a crontab where you would have to remember to delete it. at exists for that. This post covers the commands and the traps, as the one-shot counterpart to cron syntax. The commands assume at 3.2.5 on Debian 13.

Schedule a job

Shell
sudo apt install at
sudo systemctl enable --now atd
echo "/usr/local/bin/restart-app.sh" | at 02:30
Text
warning: commands will be executed using /bin/sh
job 7 at Wed Nov  4 02:30:00 2026

If 02:30 has already passed today, the job is set for tomorrow. The first line is a reminder that the commands run under /bin/sh, so Bash-specific syntax will not work unless you call bash explicitly. The job number is what you use to inspect or remove it. Without the daemon running, at accepts jobs and never runs them, the most common surprise on a fresh install.

Time specifications

Shell
at now + 45 minutes
at noon tomorrow
at 4pm + 3 days
at 10:00 2026-12-01
at midnight
at -f /home/ada/cleanup.sh teatime

at understands relative offsets, named times (noon, midnight, teatime for 4 pm) and dates in several formats. Interactive use ends with Ctrl-D on an empty line. -f reads the commands from a file instead of the keyboard, and -m sends mail even if the job produces no output. To check how at read a time, look at the "job N at ..." line it prints.

See, inspect, remove

Shell
atq
at -c 7 | tail -n 5
atrm 7
Text
7	Wed Nov  4 02:30:00 2026 a ada

The columns are the job number, the run time, the queue letter and the owner. at -c 7 prints the job, which is a script that first sets the environment the job was created with, then cds to the original directory and runs your commands, so you can see exactly what will happen. A normal user sees only their own jobs; root sees all.

Environment and output

A job runs with the environment and working directory of the shell where you created it, captured at that moment, but without a terminal. That differs from cron, where the environment is minimal. Anything the job prints is mailed to the owner if a mail system exists, and discarded otherwise, so redirect output yourself: ... >> /tmp/job.log 2>&1. Variables that change after scheduling, such as an expired token, are stale when the job runs.

batch and permissions

Shell
echo "/usr/local/bin/reindex.sh" | batch
cat /etc/at.deny

batch queues a job to run when the load average falls below a threshold (0.8 by default on a single core), which suits heavy work you do not want to compete with other tasks. Access control: if /etc/at.allow exists, only the listed users may use at; otherwise anyone not in /etc/at.deny may.

Where this does not apply

Jobs scheduled with at survive a reboot, since they are stored in /var/spool/cron/atjobs, but one scheduled during downtime runs at the next start. For a more observable alternative, systemd-run --on-active=2h --unit=restart-later /usr/local/bin/restart-app.sh creates a transient timer with journal logging; see listing timers. For anything repeating, use cron or a timer.

How at accepts a time

The at command reads a time specification and then the commands to run from standard input or from a file with -f. The specification is forgiving: at 17:30, at noon tomorrow, at now + 2 hours, at 9am Friday and at 14:00 2026-12-24 all work, and the accepted words are documented in the file /usr/share/doc/at/timespec on many systems. If the time has already passed today, at assumes tomorrow, which can produce a job a day later than you meant when you mistype a time. The command prints the job number and the time it understood, such as "job 12 at Tue Nov 3 17:30:00 2026", so read that line and confirm it. Jobs run with /bin/sh and not Bash, so Bashisms in a pasted command fail with a puzzling message mailed to you. Put anything complex into a script and have at call the script by full path.

Because a job is one-off, at fits tasks such as "reboot the server tonight", "send this reminder next week" and "remove this temporary firewall rule in ten minutes", the last of which is a safety net while you change remote access settings.

What environment the job gets

When you submit a job, at captures the current environment, the working directory and the umask, and recreates them when the job runs, which differs from cron's minimal environment. That is convenient, since the PATH and variables you checked with are the ones the job uses, and it carries a risk: if your shell had an unexpected variable set, the job inherits it. You can see exactly what will run with at -c JOBNUMBER, which prints the saved script including the environment preamble, and reading it before the job fires is the best check. Output that the job writes to standard output or error is mailed to the user, if a mail system is installed, and otherwise lost, so redirect output to a file in the command when you need a record. A job that needs to survive its own failure should log its result, since nobody is watching when it fires.

Listing, removing and controlling who may use it

atq lists pending jobs for you, with the number, time, queue and user, and root sees everyone's. atrm NUMBER removes one. Jobs are stored in a spool directory, usually /var/spool/cron/atjobs or /var/spool/at, so they persist across a reboot, and a job whose time passed while the daemon was down runs when it starts again. The atd service must be running, and on some minimal installs it is not even installed, which is why at appears to accept jobs that never run: check with systemctl status atd. Access is controlled by /etc/at.allow and /etc/at.deny: if the allow file exists, only the listed users may use at, and otherwise anyone not in the deny file may. The related batch command queues a job that runs when the system load is low, which suits heavy one-off tasks that should not compete with interactive work.

Typical uses and mistakes

The most useful habit with at is the safety net: before changing a firewall rule or an SSH setting on a remote machine, schedule the undo with at now + 10 minutes, make the change, and cancel the job with atrm once you confirm the connection still works. If the change locks you out, the revert runs by itself. Common mistakes are forgetting that the job runs under /bin/sh, using relative paths that resolve against the saved working directory in an unexpected way, and assuming mail is delivered on a host with no mail system, so the error output vanishes. Always check the confirmation line with the parsed time, and list the queue after submitting to see that the job is there. For anything recurring, use cron or a timer instead, since at is built for single events.

See at(1) and POSIX at.

Taggedcron
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.