when a single execution is all you need
Not every scheduled task has to be recurring. For one-off actions at a specific point in time, or dependent on system load, at and batch are the leaner tools compared to cron and systemd timers. This guide explains syntax, time expressions, job management with atq and atrm, and permission control through at.allow and at.deny.
Table of Contents
- 1. When at and batch Are the Better Choice
- 2. Installation and the atd Daemon
- 3. Basic at Syntax and Time Expressions
- 4. Managing Jobs: atq, atrm, and at -c
- 5. batch: Execution Based on System Load
- 6. Environment Variables and Working Directory in at Jobs
- 7. Controlling Permissions: at.allow and at.deny
- 8. Practical Example: a Delayed Maintenance Window
- 9. at/batch Compared to cron and systemd Timers
- 10. Summary
- 11. FAQ
1. When at and batch Are the Better Choice
Cron and systemd timers are built for recurring tasks: a backup every night, a log rotation every week, a certificate check twice a day. But as soon as a task should run exactly once, say in three hours or tomorrow at 2pm, a permanent crontab line or a dedicated systemd timer unit is unnecessary administrative overhead. This is exactly the case the at command exists for: it schedules an execution at a specific point in the future, without leaving a permanent entry in the system afterward.
A typical scenario: a maintenance window starts in two hours, a service should be shut down automatically for it, without anyone waiting at the terminal the whole time. Instead of creating a temporary crontab line and removing it again after execution, at schedules this single event directly and vanishes from the queue by itself once it has run. The batch command, the twin of at, takes a different approach: it runs a job as soon as system load drops below a certain threshold, independent of a fixed time, which suits resource-intensive one-off tasks that should not compete with ongoing production workloads.
Both tools complement cron and systemd timers rather than replacing them. For recurring tasks, cron or a systemd timer remains the right choice; for one-time, non-recurring actions, at and batch are the leaner tool, because no permanent configuration entry needs to be maintained and the job queue cleans itself up after every execution.
2. Installation and the atd Daemon
On many minimal server installations the at package is not preinstalled and must first be added via the package manager. Debian and Ubuntu use apt install at, RHEL based systems use dnf install at. After installation, the atd daemon runs in the background, periodically checking the queue of scheduled jobs and starting due tasks at the right time. Without a running atd process, scheduled at jobs are stored in the queue but never executed, which is why checking systemctl status atd should be the first step for any problem with at jobs.
The daemon itself checks the queue once per minute by default, which lets jobs start with high precision at the scheduled time, but without the real-time guarantees of a dedicated scheduler. For the vast majority of administrative tasks, such as a delayed restart or a one-time data cleanup, this precision is entirely sufficient. After installation it is also worth running systemctl enable atd, so the daemon starts automatically again after a server reboot and no scheduled jobs get left behind unnoticed.
#!/usr/bin/env bash
# Install at/atd and verify the daemon is active
set -euo pipefail
apt-get update -qq
apt-get install -y at
systemctl enable --now atd
systemctl status atd --no-pager
# Quick smoke test: schedule a trivial job one minute from now
echo 'echo "at is working" >> /tmp/at-test.log' | at now + 1 minute
3. Basic at Syntax and Time Expressions
Calling at TIME_EXPRESSION opens an interactive prompt where commands are entered line by line and terminated with Ctrl+D. In scripts, the pipe variant is more practical: echo "command" | at TIME_EXPRESSION passes the command directly without interactive input. at's time expressions are surprisingly flexible and read almost like natural language: at now + 1 hour, at 14:00, at teatime for 4pm, at midnight, at noon tomorrow, or at 09:00 next monday are all valid expressions.
For exact future points in time, combining clock time and date works well, for example at 14:00 2026-08-15. Relative time expressions such as at now + 3 days or at now + 2 weeks are especially useful for tasks whose exact timing depends on the current moment rather than a fixed calendar date. One important detail: at always interprets time expressions in the local system timezone, so a quick look at timedatectl beforehand avoids misunderstandings on servers with an unclear timezone configuration.
#!/usr/bin/env bash
# at accepts a wide range of natural-language time expressions
set -euo pipefail
# Relative time from now
echo "systemctl restart nginx" | at now + 30 minutes
# Specific clock time today (or tomorrow, if already past)
echo "/usr/local/bin/generate-report.sh" | at 23:30
# Named times
echo "/usr/local/bin/nightly-cleanup.sh" | at midnight
echo "/usr/local/bin/send-reminder.sh" | at teatime
# Specific date and time
echo "/usr/local/bin/cert-renewal-check.sh" | at 09:00 2026-08-15
# Relative days/weeks
echo "/usr/local/bin/decommission-old-host.sh" | at now + 2 weeks
4. Managing Jobs: atq, atrm, and at -c
Once several jobs are scheduled, atq gives an overview of the current queue: each line shows a job number, the scheduled time, and the queue the job is assigned to. This job number is the key for every further operation. To remove a scheduled but not yet executed job, use atrm JOBNUMBER, which becomes especially important when plans change on short notice and an already scheduled action should no longer happen.
For troubleshooting, at -c JOBNUMBER is indispensable: the command outputs the full content of a scheduled job, including all environment variables frozen at scheduling time and the actual command to be executed. This is especially helpful when an at job does not behave as expected, since the cause is often an environment variable that was set differently at scheduling time than expected at execution time.
#!/usr/bin/env bash
# Inspect, list, and remove pending at jobs
set -euo pipefail
# List all pending jobs with their job number and scheduled time
atq
# Example output:
# 3 Fri Aug 15 09:00:00 2026 a root
# 4 Sat Aug 16 00:00:00 2026 a root
# Show the exact frozen environment and command of job 3
at -c 3
# Remove a job that is no longer needed
atrm 4
# Confirm removal
atq
5. batch: Execution Based on System Load
While at requires a fixed point in time, batch skips a time expression entirely and instead waits until system load drops below a configured threshold, by default a load average of 1.5 on most distributions. This is excellent for resource-intensive one-off tasks like a large database reindex, an extensive image conversion, or a log analysis over several gigabytes that should not compete with ongoing production workloads for CPU time.
The call works syntactically identical to at, just without a time expression: echo "command" | batch. The threshold itself can be adjusted via the -l parameter when starting the atd daemon, usually through an init configuration file or a systemd override. On servers with consistently high baseline load, such as heavily used database servers, deliberately adjusting this threshold is worthwhile, since batch jobs otherwise never get to run if load average is permanently above the default value.
#!/usr/bin/env bash
# batch: run when system load drops below the configured threshold
set -euo pipefail
# Schedule a resource-intensive reindex job to run when load permits
echo "/usr/local/bin/reindex-search-catalog.sh" | batch
# batch jobs show up in the same queue as at jobs
atq
# Check current load average to understand when batch will fire
uptime
# Adjust the load-average threshold atd uses for batch jobs
# (Debian/Ubuntu: edit /etc/default/atd or the systemd unit override)
# ExecStart=/usr/sbin/atd -b -l 3.0 -f
6. Environment Variables and Working Directory in at Jobs
A common pitfall: at freezes environment variables and the current working directory exactly at scheduling time, not at the later execution time. If an at job is scheduled during an interactive SSH session with certain environment variables set, but the session ends before execution, exactly these frozen values persist, even if system wide environment variables have changed in the meantime. This fundamentally differs from cron, where jobs start with a minimal, defined environment by default, regardless of who edited the crontab and when.
For scripts that depend on specific environment variables, it is therefore advisable to set them explicitly inside the scheduled command, rather than relying on the environment frozen at scheduling time. The working directory is frozen as well: an at job using relative paths always references the directory from which at was originally called, not the home directory or another default directory at execution time.
#!/usr/bin/env bash
# Explicit environment inside the scheduled command,
# instead of relying on the frozen environment from planning time
set -euo pipefail
echo "cd /var/www/html && APP_ENV=production /usr/bin/php artisan cache:clear" | at now + 1 hour
# Verify what got frozen: working directory and environment
at -c "$(atq | tail -1 | awk '{print $1}')" | head -20
7. Controlling Permissions: at.allow and at.deny
By default, every local user is allowed to create their own at jobs, which is not always desirable on a multi-user system or a server with several deploy accounts. The files /etc/at.allow and /etc/at.deny control this permission centrally: if at.allow exists, only the users listed there may create at jobs, everyone else is rejected. If no at.allow exists but an at.deny does, all users except those explicitly blocked there may use at.
For production servers with a strict permission model, an explicit at.allow file with exactly the accounts allowed to create at jobs is the safer variant, following the principle of least privilege. Root always retains access to at regardless of either file, this restriction applies exclusively to regular user accounts. No daemon restart is needed after changing these files, since atd re-reads the permission files directly on every call to at.
# /etc/at.allow — if this file exists, ONLY listed users may use at/batch
# One username per line, comments not supported by all implementations
deploy
backup-operator
# /etc/at.deny — used only when at.allow does NOT exist
# Explicitly blocks listed users, everyone else is allowed
# guest
# nobody
# Root can always schedule at/batch jobs regardless of either file.
# Changes take effect immediately, atd re-reads these files per invocation.
8. Practical Example: a Delayed Maintenance Window
A realistic scenario from day to day operations: a planned maintenance window starts in two hours. A deployment script should automatically put the server into maintenance mode, without anyone waiting the whole time to run the command manually. With at this one-time event can be scheduled precisely for the right moment, including an automatic notification that the maintenance window has begun.
Combined with a rollback mechanism that is also scheduled via at when needed, this creates a complete, time-bounded maintenance scenario without permanent cron entries. The big advantage over a temporarily created cron line: after execution the at job automatically vanishes from the queue, leaving no cleanup step for a manually removed cron entry.
#!/usr/bin/env bash
# schedule-maintenance.sh — schedule maintenance start and automatic rollback
set -euo pipefail
readonly MAINTENANCE_START="now + 2 hours"
readonly MAINTENANCE_END="now + 4 hours"
# Enter maintenance mode and notify the team when the window begins
{
echo "touch /var/www/html/maintenance.flag"
echo "systemctl reload nginx"
echo 'curl -fsS -X POST -d "Maintenance window started" https://hooks.example.com/notify'
} | at "$MAINTENANCE_START"
# Automatic rollback: exit maintenance mode after the window closes,
# even if nobody is available to run the command manually
{
echo "rm -f /var/www/html/maintenance.flag"
echo "systemctl reload nginx"
echo 'curl -fsS -X POST -d "Maintenance window ended" https://hooks.example.com/notify'
} | at "$MAINTENANCE_END"
atq
9. at/batch Compared to cron and systemd Timers
The choice between at, batch, cron, and systemd timers mostly depends on whether a task is one-off or recurring, and whether a fixed point in time or system load should determine the execution moment.
| Tool | Repetition | Trigger | Typical Use |
|---|---|---|---|
| at | One-off, no repetition | Fixed point in time | Delayed restart, planned maintenance window |
| batch | One-off, no repetition | System load drops below threshold | Resource-intensive one-off jobs |
| cron | Recurring on a schedule | Fixed time fields in crontab | Daily backups, log rotation |
| systemd timer | Recurring, flexible | OnCalendar, OnBootSec, OnUnitActiveSec | Recurring jobs with journal integration |
| Temporary crontab line | Manual creation and removal needed | Fixed point in time | Unnecessary overhead compared to at |
As a rule of thumb: recurring tasks belong in cron or a systemd timer, one-off tasks with a fixed point in time belong to at, and resource-intensive one-off tasks without a fixed time belong to batch. A temporarily created and later removed crontab line for a one-off event is in practically every case the worse solution compared to a single at call.
Mironsoft
Deployment automation and scheduling concepts for Linux servers
One-time tasks without permanent cron entries?
We analyze existing maintenance and deployment scripts and set up fitting at, batch, or systemd timer solutions, including permission control via at.allow and at.deny.
Scheduling Audit
Evaluating existing cron entries for actual repetition needs
at/batch Integration
Embedding maintenance windows and one-off tasks cleanly into deployment pipelines
Permission Model
Configuring at.allow and at.deny according to the principle of least privilege
10. Summary
at and batch close a gap that cron and systemd timers deliberately leave open: one-time, non-recurring tasks without a permanent configuration entry. at TIME_EXPRESSION schedules an event at a specific moment in near natural language, batch instead waits for low system load. atq shows the queue, atrm removes individual jobs, at -c shows the frozen environment of a scheduled job for troubleshooting.
Permissions can be controlled centrally via /etc/at.allow and /etc/at.deny, without needing to restart the atd daemon. For delayed maintenance windows, one-time rollbacks, or resource-intensive one-off jobs, at and batch are the leaner tool compared to a temporarily created and later removed crontab line.
at and batch, the Essentials at a Glance
One-Off, Not Recurring
at and batch are meant for exactly one execution, cron and systemd timers for recurring schedules.
Flexible Time Expressions
at now + 1 hour, at teatime, at 09:00 next monday read almost like natural language.
Job Management
atq lists, atrm removes, at -c shows a job's frozen environment.
batch for Load Sensitivity
batch waits for low system load instead of a fixed time, ideal for resource-intensive one-off jobs.