Subtopic•Handlers

What Are Handlers

Throughout this guide, the phrase "notify a handler" has appeared dozens of times — in template tasks, in role configuration tasks, in service management. Now we focus on handlers as a system in their own right, because understanding how they work, when they fire, and what guarantees they provide is essential for writing correct Ansible automation.

The Core Concept

A handler is a special task that only runs when explicitly notified by another task — and only when that notifying task reports a changed result. Handlers are Ansible's way of implementing the rule: "only restart a service if something that affects it actually changed."

Consider what happens without handlers. If you deploy a configuration file and then unconditionally restart the service, two problems emerge. First, services restart on every playbook run, even when nothing changed — unnecessary downtime, unnecessary disruption to active connections. Second, if multiple tasks each restart the service, you restart it three times when once would have been sufficient.

Handlers solve both problems: they fire only on actual changes, and each handler fires at most once per play regardless of how many tasks notify it.


How Handlers Are Different from Regular Tasks

Regular tasks:

  • Run unconditionally on every playbook execution (unless when: skips them)
  • Run in the order they appear in the task list
  • Run immediately when their position in the task list is reached

Handlers:

  • Run only when notified by a task that produced a changed result
  • Run after all tasks in the play have completed
  • Run only once per play, even if notified by multiple tasks
  • Run in the order they are defined in handlers:, not the order they were notified

Defining and Notifying Handlers

Handlers are defined in the handlers: section of a playbook or in a role's handlers/main.yml. They look exactly like regular tasks. The connection between a task and a handler is made through the handler's name: field:

# handlers/main.yml or playbook handlers: section

- name: Reload Nginx
  ansible.builtin.service:
    name: nginx
    state: reloaded

- name: Restart Nginx
  ansible.builtin.service:
    name: nginx
    state: restarted

- name: Restart application
  ansible.builtin.systemd:
    name: my-app
    state: restarted
    daemon_reload: yes

Tasks notify handlers by their exact name:

tasks:
  - name: Deploy Nginx main configuration file
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: Reload Nginx       # triggers "Reload Nginx" handler if task changed

  - name: Deploy Nginx virtual host config
    ansible.builtin.template:
      src: vhost.conf.j2
      dest: /etc/nginx/sites-available/default.conf
    notify: Reload Nginx       # same handler - still only fires once

  - name: Deploy new application binary
    ansible.builtin.copy:
      src: my-app
      dest: /usr/local/bin/my-app
      mode: '0755'
    notify: Restart application

In this sequence: both template tasks notify Reload Nginx. The binary copy notifies Restart application. After all tasks complete, Reload Nginx fires once and Restart application fires once. If both template tasks reported ok (no change), Reload Nginx does not fire at all.


Handler Execution Timing: End of Play

By default, handlers run at the end of the play after all tasks have finished. This means if your play has 50 tasks and task 5 notifies Reload Nginx, the handler does not run until after task 50 completes.

The practical implication: if task 5 changes the Nginx config and notifies a reload, but task 30 also needs to add a virtual host before Nginx is reloaded, everything works correctly — the reload happens after task 30 completes, so Nginx gets both changes in one reload.

If you need a handler to fire immediately at a specific point in the task list (not at the end), use ansible.builtin.meta: flush_handlers:

tasks:
  - name: Deploy Nginx config
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: Reload Nginx

  - name: Force handlers to run now before continuing
    ansible.builtin.meta: flush_handlers
    # Reload Nginx fires here, immediately

  - name: Run smoke test against newly reloaded Nginx
    ansible.builtin.uri:
      url: "http://localhost/health"
      status_code: 200

flush_handlers causes all pending handler notifications to be processed immediately at that point in the task list. After flushing, the handler queue is empty — if the same handler is notified again later, it will fire again at the end.

The next section covers the full mechanics of creating and naming handlers — including naming conventions, handler chaining (handlers that notify other handlers), and how to structure handlers for maintainability.

←Previous ModuleRoles