skip to content

In an Ansible playbook, when do handlers actually run, and what has to happen for a handler to fire at all?

level: middleimportance: must knowfreq 72%

answer

  1. fired by change, not by call
  2. queued, never immediate
  3. end of play, once per host
  4. definition order beats notify order
  5. meta flush_handlers runs them early

basics

~20 s

A handler runs only when a task that notifies it reports changed, and it runs once, at the end of the play, not at the point of notification. A meta flush_handlers task forces queued handlers to run earlier.

solid answer

~50 s

Handlers are tasks that are not executed in line. A regular task with `notify: restart nginx` only queues that handler, and only if the task's result was **changed** — an `ok` result notifies nothing. Ansible then runs the queued handlers once each, at the end of the play, in the order they are *defined* in the handlers section, not the order they were notified. Ten tasks notifying the same handler still produce one restart, which is the whole point: you edit five config files and bounce the service once. `notify` matches on the handler's name, or on a `listen` topic so one notification can fan out to several handlers. If you need the handler to run mid-play — say, restart the service before a later task talks to it — insert `- meta: flush_handlers`. And by default a host that fails later in the play never runs its pending handlers unless you set `force_handlers: true` or pass `--force-handlers`.

go deeper

for a junior

Know that handlers are restart-style tasks triggered by notify, and that they only fire when the notifying task actually changed something. Be able to write a template task with notify plus the matching handler.

for a middle

Explain the mechanics out loud: notification queues the handler, it is de-duplicated by name, it runs once at the end of the play in handlers-section order, and meta flush_handlers drains the queue early.

for a senior

Show the operational judgment — handlers are skipped on a failed host unless force_handlers is set, and a command task that always reports changed silently turns a conditional restart into a restart on every run of production.

for a principal

Own the convention across the estate: whether roles expose listen topics rather than handler names, when a restart belongs in a handler versus an explicit orchestrated step with serial batching, and how that choice affects blast radius during a fleet-wide config change.

## What a handler is A handler is an ordinary Ansible task that lives in the play's `handlers:` section (or a role's `handlers/main.yml`) and is not run in sequence with the other tasks. It sits idle until something notifies it. The canonical use is a service restart: several tasks may change the configuration of a daemon, but the daemon should be restarted once, after all of those edits, and not at all if nothing actually changed. ```yaml - hosts: web tasks: - name: Install nginx config ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf notify: restart nginx handlers: - name: restart nginx ansible.builtin.service: name: nginx state: restarted ``` ## The firing rule: changed, not called A handler fires only when the notifying task reports **changed**. This is the hinge that connects handlers to idempotency. On the first run the template is written, the task is `changed`, the handler is queued. On the second run the file already matches, the task is `ok`, nothing is queued, and nginx is not restarted. A skipped task (its `when` was false) also notifies nothing. This is also why handlers are so often broken by a task that lies about its result. A `command` or `shell` task reports changed unconditionally, so a handler notified by one restarts the service on every single run — the playbook is no longer idempotent even though every module in it is. ## When they run Queued handlers run at the **end of the play**, after the last task of that play, for the hosts that notified them. Two properties follow: - **De-duplication.** Notifying the same handler from ten tasks runs it once. Handlers are keyed by name. - **Definition order, not notification order.** If tasks notify `restart nginx` then `reload firewall`, but the handlers section lists `reload firewall` first, the firewall handler runs first. Candidates routinely get this wrong. Each host carries its own notification list, so a change on one host restarts that host's service and leaves the others alone. ## notify and listen `notify` takes a handler name or a list of names. `listen` inverts the relationship: several handlers subscribe to a topic string, and a task notifies the topic. ```yaml handlers: - name: restart nginx ansible.builtin.service: { name: nginx, state: restarted } listen: "restart web stack" - name: restart php-fpm ansible.builtin.service: { name: php-fpm, state: restarted } listen: "restart web stack" ``` This decouples the notifier from the handler names, which matters when the handlers live in a role that the notifying playbook does not own. ## Forcing them early with flush_handlers Sometimes end-of-play is too late — you changed a config and the next task must talk to the reconfigured service. `- meta: flush_handlers` runs every currently queued handler at that point and empties the queue; the play then continues normally, and later notifications queue again for the end. ```yaml - name: Apply config now, before the smoke test ansible.builtin.meta: flush_handlers ``` ## Failure and handlers By default, if a host fails a task, it drops out of the play and its **pending handlers never run**. That is deliberate: you usually do not want to restart a service after a half-applied change. When you do want them anyway (for example, a lock or cleanup step expressed as a handler), set `force_handlers: true` on the play, or run with `--force-handlers`. ## Things that bite in real repositories - Renaming a handler and forgetting a `notify` elsewhere: the notification silently matches nothing rather than erroring in older behaviour, so the restart quietly stops happening. Prefer `listen` topics for cross-role notification. - Two handlers defined with the same name: only one of them is reachable by that name. - Expecting a handler to run per notification — it runs once per host per play. - Handlers notified by `command`/`shell` tasks firing on every run, because those modules always report changed. - Running with `--check`: tasks report the change they *would* make, so handlers are queued and reported but the handler itself runs in check mode too, which for a service restart is a no-op — do not read a check-mode handler line as proof the restart works.

  • A task notified a handler, and then a later task on that host failed. Does the handler still run?
    No. By default a failed host is removed from the play and its queued handlers are discarded, which is usually what you want after a half-applied change. If the handler must run regardless — a cleanup or unlock step — set `force_handlers: true` on the play or pass `--force-handlers` on the command line.
  • Five tasks in one play notify the same restart handler. How many restarts happen on each host?
    One. Handlers are de-duplicated by name per host, so the service is restarted once at the end of the play no matter how many tasks notified it. That is precisely why config edits are expressed as several tasks notifying one handler rather than each doing its own restart.
  • What does `listen` give you that notifying by name does not?
    `listen` makes the handler subscribe to a topic instead of being addressed by its own name, so one `notify: restart web stack` can fan out to several handlers, possibly defined in different roles. It decouples the notifying task from handler names you do not control, and lets you add another handler to the topic without editing any notifier.

saying these in an interview costs you the question

  • Says a handler runs immediately after the task that notifies it
  • Thinks ten notifications produce ten handler runs
  • Believes handlers run in the order they were notified
  • Assumes a handler fires even when the task reported ok
  • Expects pending handlers to run after the host has failed

context