unit 5 · Logic

Handlers, notify & listen

Ten config changes, one restart

essential · 6 min · linux + windows

you may want to read 12 · Playbook anatomy first — but nothing is locked

tl;dr

  • notify: queues a handler — a task that runs only when the notifying task reports changed.
  • Queued handlers run once, at the end of the play, however many tasks notified them.
  • listen: turns the notify string into a topic several handlers can subscribe to.

why it matters Restarting a service every run is needless downtime; never restarting leaves stale config live — handlers restart exactly when something changed, and only once.

Lesson 12’s playbook shipped with a hole it admitted to: nothing reacted when the copy changed a file. Point that copy at nginx’s own config and the hole bites. A running nginx serves the config it loaded at startup. Rewrite the file and the process ignores it. state: started won’t help; it sees the service up and says ok.

Both naive fixes are bad. state: restarted bounces a healthy service on every run — downtime for nothing, and a permanent changed in your recap. Registering the copy result and gating a restart on it works, but it’s plumbing for a pattern so common Ansible built it in: the handler .

A handler is a task like any other — name, module, args — written in a play’s handlers: section instead of tasks:. It doesn’t run in order with the play. It runs only when a task notify -s it, and a task only notifies when it reports changed:

- name: Configure web servers
  hosts: web
  become: true
  tasks:
    - name: Push nginx config     # changed → queues the handler
      ansible.builtin.copy:
        src: files/nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: Restart nginx       # must match the handler's name exactly
  handlers:
    - name: Restart nginx         # runs ONCE, after the last task
      ansible.builtin.service:
        name: nginx
        state: restarted
lesson 12's play, done right — copy notifies, handler restarts

Read the flow. copy compares the file; when it rewrites it — changed — the notification is queued. ok queues nothing. Run it again with the file already correct: ok, no notification, no restart. Restart-on-change, idempotently, for one extra line per task.

Once, at the end

The design insight: notifications are batched and deduplicated. A handler doesn’t run when notified. Every queued handler runs after the last task of the play, once each, no matter how many tasks notified it. A play that templates ten nginx config files, each notifying Restart nginx, restarts nginx exactly once.

Two mechanics to burn in. Matching is by exact string: notify: must equal the handler’s name: character for character, and notifying a handler that doesn’t exist fails the run. Handlers execute in the order they’re defined under handlers:, not the order they were notified. And when end-of-play is too late — a later task needs the service already restarted — drain the queue early with a meta: flush_handlers task.

One notification, many handlers

notify matches names one-to-one. When a single change should trigger several reactions, listen: turns the string into a topic that any number of handlers subscribe to:

handlers:
  - name: Restart nginx
    ansible.builtin.service:
      name: nginx
      state: restarted
    listen: web stack changed     # subscribes to the topic
  - name: Flush app cache
    ansible.builtin.command: /usr/local/bin/flush-cache
    listen: web stack changed     # same topic, also fires
one notify: 'web stack changed' fires both

The notifying task writes notify: web stack changed once. Every subscribed handler runs, in definition order. This earns its keep in lesson 24, where roles ship handlers in their own handlers/ directory — a topic lets your play trigger a role’s reactions without knowing its handler names.

One last boundary: the handler doesn’t replace lesson 12’s service task. state: started still belongs in tasks: to guarantee the service is up at all; the handler owns restarts. Tasks declare state, handlers react to change — hold that split and every handlers: block you meet reads instantly.

replay — Two notifications, one restart

a recording, not a simulator — tap ⓘ lines for what they mean

self-check — recall, not a test

  1. 1 A play templates ten nginx config files, each with notify: Restart nginx. How many restarts, and when?

  2. 2 You re-run the play; the config file is unchanged. Does the handler fire? Why not?

  3. 3 Your config task reported changed, a later task failed, and now re-runs never restart the service. What happened?

lab (optional — never required)

Take your lesson 12 site.yml, add a Restart nginx handler notified by the copy task, run twice, and confirm the handler fires on run one only.

needs: The control node from lesson 5.

next up 21 · Blocks & error handling 7 min · unit 5