LearnRHCSA (EX200)Deploy, Configure, Maintain

systemd Timers

systemctl - a hands-on Linux lab on a real virtual machine.

Schedule work the modern RHEL 10 way with systemd timers. A .timer unit holds the schedule and starts a .service of the same name that does the work. Read what is scheduled with systemctl list-timers, read a timer's real schedule with systemctl cat (OnCalendar= sets when, Persistent=true catches up a missed run at boot), and arm it at boot plus start it now with systemctl enable --now name.timer. On RHEL 10 the daily log rotation runs from logrotate.timer, not cron. Serves EX200 deploy-configure: schedule tasks and manage the systemd path RHEL 10 uses.

It is Monday morning. A task told you to make a maintenance script run every night at midnight, and you set it up on Friday. You come back to find the log has three days of silence in it. The machine was powered off over the weekend, so midnight came and went with nobody home. The schedule fired into an empty room, and nothing caught up when the machine woke.

On older systems that was just how scheduling worked: miss the window, miss the run. RHEL 10 has a smarter scheduler built into systemd, the same system that starts every service on the machine. It is called a timer, and it can remember a run it missed while the machine was off and do it on the next boot. This lesson takes you from never having seen a timer to reading, inspecting, and enabling one, using systemctl, the same tool you already use to manage services.

The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. The outputs are from a real RHEL 10 machine (AlmaLinux 10.2). A few values, like the NEXT and LAST timestamps and the time left, depend on the machine's clock and when it last ran, so treat those as illustrative. What is stable everywhere is the SHAPE: systemctl list-timers prints one row per timer, and systemctl cat prints a unit's real configuration.

A timer is a unit that tells systemd to start something on a schedule. Picture an alarm clock wired to a light switch. The alarm clock is the timer: it does nothing on its own except watch the clock. When the time comes, it flips a switch. The switch turns on the thing that does the actual work.

That is the key idea, and it trips up almost everyone the first time: a timer never does any work itself. It comes in a pair. There is a .timer unit that holds the schedule, and a .service unit of the SAME NAME that holds the command to run. logrotate.timer says when. logrotate.service says what. When the clock strikes, the timer starts its matching service, and the service does the job.

For decades the answer to run this on a schedule was cron, a separate scheduling system with its own file format. Cron still works and you will meet it in its own lesson. But RHEL 10 leans on systemd timers for its own housekeeping, and you should too.

Three reasons a timer wins. First, catch-up: a timer can run a job it missed while the machine was off, which cron cannot do. Second, one system: the timer's logs land in the same journalctl you already read for every other service, instead of a separate cron mail. Third, dependencies: because the work runs as a normal service unit, it can wait for the network or a mounted disk before it fires. The clearest proof that this is the modern default: on RHEL 10, the daily log rotation that keeps /var/log from filling the disk runs from logrotate.timer, not from cron.

The first thing you do on any machine is ask what is already scheduled. The command is systemctl list-timers. It prints one row per active timer, sorted by which fires next. The --no-pager flag tells it to print straight to the screen instead of opening a scrolling pager, which is what you want when you are just reading one screen.

Before you run it, know what to look for: a row whose UNIT ends in .timer and whose ACTIVATES column names the .service it will start. Ask the machine what is scheduled:

systemctl list-timers --no-pager

prompt: [root@servera ~]# answer: systemctl list-timers --no-pager output: NEXT LEFT LAST PASSED UNIT ACTIVATES Mon 2026-07-06 00:37:00 UTC 3h 20min Sun 2026-07-05 17:29:48 UTC - logrotate.timer logrotate.service hint: The subcommand is list-timers. Add --no-pager so it prints straight to the screen: systemctl list-timers --no-pager

Read the row left to right. NEXT is when the timer will fire next: Mon 2026-07-06 00:37:00 UTC. LEFT is how long until then: 3h 20min. LAST is when it last ran: Sun 2026-07-05 17:29:48 UTC. PASSED is how long ago that was. UNIT is the timer itself, logrotate.timer, and ACTIVATES is the service it starts when it fires, logrotate.service. That last pair is the whole model in one line: the .timer on the left activates the .service on the right. This is the RHEL 10 log rotation, scheduled by a timer, not by cron.

The NEXT and LAST timestamps and the LEFT and PASSED columns will read differently on your machine, because they depend on the current time and when the timer last ran. That is expected. The stable part is the two ends of the row: a UNIT ending in .timer and the .service it ACTIVATES.

The list tells you a timer exists. To see HOW it is scheduled, you read the unit file itself. The command is systemctl cat <name>, and it prints the real, on-disk configuration of any unit, timer or service. It is the honest answer to what does this unit actually say, with no guessing.

Read the configuration of the log rotation timer:

systemctl cat logrotate.timer

prompt: [root@servera ~]# answer: systemctl cat logrotate.timer output: Description=Daily rotation of log files [Timer] OnCalendar=daily Persistent=true hint: The subcommand is cat, then the unit name including the .timer suffix: systemctl cat logrotate.timer

Two lines carry the whole schedule. OnCalendar=daily is the schedule: daily is a shorthand that means every day at midnight, 00:00. You can write real calendar expressions here too, like OnCalendar=Mon *-*-* 09:00:00 for every Monday at 9 in the morning, but daily, weekly, and hourly cover most needs. Persistent=true is the catch-up switch: it tells systemd that if the machine was off when the timer should have fired, run the job once at the next boot. That single line is why a timer beats cron for a job that must not be skipped. The [Timer] header just marks the section that holds these settings.

OnCalendar= accepts more than the daily shorthand: a full expression is DayOfWeek Year-Month-Day Hour:Minute:Second, where a * means every. Do not memorize the grammar now. The one form to know cold is that daily equals midnight every day, and that Persistent=true is the line that makes a missed run catch up.

A timer that exists on disk does nothing until it is enabled. Enabling a timer means two separate things, and the exam cares about both. Enable wires it to start automatically on every boot. Start (which systemctl calls --now) starts it right now, this session, without waiting for a reboot. The one command that does both at once is systemctl enable --now <name>.

This is the exact same enable --now you use for services like sshd and chronyd. That is the payoff of timers living inside systemd: one verb, one habit, for every schedulable thing on the machine. Turn a timer on both ways at once:

systemctl enable --now logrotate.timer

prompt: [root@servera ~]# answer: systemctl enable --now logrotate.timer output: hint: The verb is enable, the flag --now starts it immediately, then the .timer unit: systemctl enable --now logrotate.timer

This timer was already enabled, so systemctl printed nothing, and no output is success here. On a timer that was not yet enabled you would see a line reporting a symlink being created in the timers.target.wants directory, which is systemd wiring the timer into the boot sequence. Two things to hold onto: enable alone would arm it for the next boot but leave it dormant now, and --now is what starts it in this session too. Enable arms it for every future boot; --now starts it this instant. To confirm it is armed, run systemctl list-timers again and look for your timer in the list.

The classic timer mistake is enabling the wrong half of the pair. You enable the .timer unit, never the .service. If you run systemctl enable --now logrotate.service you run the log rotation once, right now, and arm nothing for the future. The .service is the worker; the .timer is the schedule. When a task says schedule this, your target always ends in .timer.

Scaffolding off. No command is printed this time. You have every piece you need.

A task hands you a machine and says: the daily log rotation is running from a timer, and I need to know its exact schedule and whether it catches up after downtime. You are not asking when it fires next, so this is not the list command. You want the timer's real on-disk configuration, the OnCalendar= and Persistent= lines, printed straight from the unit file. Which single systemctl subcommand, given the unit name logrotate.timer, prints that configuration?

prompt: [root@servera ~]# answer: systemctl cat logrotate.timer output: Description=Daily rotation of log files [Timer] OnCalendar=daily Persistent=true hint: It is not list-timers. The subcommand that prints a unit's on-disk file is cat: systemctl cat logrotate.timer

systemctl cat logrotate.timer prints the unit file itself. OnCalendar=daily answers the schedule, midnight every day, and Persistent=true answers the catch-up question, yes it runs a missed job at the next boot. list-timers would have told you the NEXT fire time, but only cat shows you the rules that produce it. When a task asks how is this scheduled, systemctl cat is the command that reads the source of truth.

You earned this cheat sheet. Every row is a form you just ran or read:

The two things to burn in for the exam: a timer is a .timer unit paired with a .service of the same name, and you always enable the .timer, never the .service. OnCalendar= sets the schedule and Persistent=true makes a missed run catch up at the next boot.

systemd timers and cron solve the same problem two ways. Reach for a timer when a job must survive downtime (Persistent), when you want its logs in journalctl, or when it must wait for the network or a disk. Reach for cron when a task explicitly says use cron, or for a quick per-user job. The cron lesson covers that side. Knowing both, and when each fits, is the exam skill.

The practice terminal walked you through the whole loop of running a timer. systemctl list-timers to see what is scheduled, systemctl cat to read a timer's real schedule and its catch-up setting, and systemctl enable --now to arm it at boot and start it in one command. Every one of those you typed yourself.

The deploy-configure module mission, Operation Rollout, is where you run these against a real RHEL 10 machine. A full VM boots for you, with real timers and services waiting. The mission hands you objectives that deploy and configure the machine the way the exam does. One difference from this lesson: the mission shows no commands. You read the objective, recall the list-timers, cat, and enable --now forms, and type them. That recall is what makes it stick on exam day.

Finish the other deploy-configure lessons, then go schedule work on a real machine.

Practice systemd Timers in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.