at and batch: Scheduling One-Time Tasks on Linux
AI generated
$
/etc
Linux · at · batch · Scheduling
at and batch
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.

13 min read at · batch · atq · atrm · atd Linux · Debian/Ubuntu · RHEL

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.

11. FAQ: at and batch on Linux

1When to use at instead of cron?
For exactly one execution at a point in time, without a permanent configuration entry.
2Difference between at and batch?
at uses a fixed time, batch waits for low system load.
3Why is my job not executing?
Usually atd is not running. Check with systemctl status atd, enable with enable --now.
4How do I see all scheduled jobs?
atq shows all jobs with number and time.
5How do I remove a job?
atrm JOBNUMBER removes it from the queue.
6What environment does my job see?
The environment frozen at scheduling time, visible with at -c JOBNUMBER.
7How do I restrict at usage?
Via /etc/at.allow as an allowlist or /etc/at.deny as a blocklist.
8Can at do recurring jobs too?
No, cron or systemd timers are meant for that.
9What threshold does batch use?
By default around a load average of 1.5, adjustable via the atd parameter -l.
10Restart needed after changing at.allow?
No, atd re-reads the file on every call.