Skip to main content
RunBook Academy

AnsibleXVI · HandlersHandlers

Handler ordering and flush_handlers

Intermediate⏱ ~20 minansible-playbook

What you'll learn

  • State the three flush points in a play and what runs between them
  • Predict handler execution order from the handlers section, not the notify order
  • Use meta: flush_handlers to force a handler to run at a chosen point
  • Recognise when a dependency between handlers needs a flush rather than an ordering assumption

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Two facts about handler execution cause most of the confusion in this part. Neither is obscure, and neither is guessable:

  1. Handlers do not run where they were notified. They run at a flush point, and a play has three of them by default.
  2. Handlers do not run in the order they were notified. They run in the order they were defined in the handlers section.

The second one is the surprise. It is also easy to demonstrate, so this lesson demonstrates it rather than asserting it.

The three flush points

The execution-order lesson established the sections of a play. Handlers attach to them like this:

pre_tasks  →  flush  →  roles + tasks  →  flush  →  post_tasks  →  flush

Anything notified during pre_tasks runs before the first role starts. Anything notified by a role or a task runs after the last task in the play body. Anything notified during post_tasks runs at the very end. A notification cannot cross a flush point backwards, and it does not survive to a later section — it is consumed at the first flush that follows it.

Read-only / Safethe three flush points, executed
$ ansible-playbook -i inv.ini flush.yml
TASK [A pre_task that changes] *************************************************
changed: [localhost]
RUNNING HANDLER [Pre handler] **************************************************
ok: [localhost]
TASK [First task changes and notifies twice] ***********************************
changed: [localhost]
TASK [Force the queued handlers to run now] ************************************
RUNNING HANDLER [Main handler] *************************************************
ok: [localhost]
TASK [A task after the flush] **************************************************
changed: [localhost]
RUNNING HANDLER [Main handler] *************************************************
ok: [localhost]
TASK [A post_task that changes] ************************************************
changed: [localhost]
RUNNING HANDLER [Post handler] *************************************************
ok: [localhost]

Three things in that output are worth pausing on.

The pre_tasks handler ran before the play body, not at the end. The task in the middle notified Main handler twice in one notify list and the handler ran once — de-duplication is real and it is per flush, not per play, which is why Main handler appears a second time after the later task notified it again. And the explicit meta: flush_handlers produced a handler run in the middle of the tasks section, which is the subject of the second half of this lesson.

Definition order, not notification order

This is the fact that catches people. The handlers section is an ordered list, and that list is the execution order.

Read-only / Safethe ordering test
- name: Handler ordering demonstration
hosts: local
gather_facts: false
tasks:
  - name: Notify the third handler first
    ansible.builtin.debug:
      msg: notify C
    changed_when: true
    notify: Handler C

  - name: Notify the first handler second
    ansible.builtin.debug:
      msg: notify A
    changed_when: true
    notify: Handler A

  - name: Notify the second handler last
    ansible.builtin.debug:
      msg: notify B
    changed_when: true
    notify: Handler B

handlers:
  - name: Handler A
    ansible.builtin.debug:
      msg: A ran
  - name: Handler B
    ansible.builtin.debug:
      msg: B ran
  - name: Handler C
    ansible.builtin.debug:
      msg: C ran
Read-only / Safeexecuted on ansible-core 2.21.3
$ ansible-playbook -i inv.ini order.yml
TASK [Notify the third handler first] ******************************************
changed: [localhost]

TASK [Notify the first handler second] *****************************************
changed: [localhost]

TASK [Notify the second handler last] ******************************************
changed: [localhost]

RUNNING HANDLER [Handler A] ****************************************************
ok: [localhost]

RUNNING HANDLER [Handler B] ****************************************************
ok: [localhost]

RUNNING HANDLER [Handler C] ****************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=6    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Notified C, A, B. Executed A, B, C. The handlers list is the schedule.

Forcing a flush: meta: flush_handlers

ansible.builtin.meta with the value flush_handlers runs every handler currently notified for that host, at the point where the meta task appears, and clears the notification set.

Service impact possiblea dependency that needs a flush
- name: Deploy and verify the reverse proxy configuration
hosts: webservers
become: true
tasks:
  - name: Deploy the site configuration
    ansible.builtin.template:
      src: site.conf.j2
      dest: /etc/nginx/conf.d/site.conf
      mode: '0644'
    notify: Reload nginx

  - name: Apply the new configuration before anything checks it
    ansible.builtin.meta: flush_handlers

  - name: Confirm the service answers on the new listener
    ansible.builtin.uri:
      url: 'http://127.0.0.1:8443/healthz'
      status_code: 200
    changed_when: false
    retries: 5
    delay: 2

handlers:
  - name: Reload nginx
    ansible.builtin.systemd_service:
      name: nginx
      state: reloaded

This is the honest use of flush_handlers: a later task in the same play depends on the handler having already run. Without the flush, the health check runs against a process still holding the previous configuration, passes, and tells you nothing.

Three properties of the meta task are worth knowing before you rely on it:

  • It flushes whatever is currently notified, not a named handler. There is no “flush just this one”.
  • It runs the notified handlers in definition order, exactly as an end-of-section flush does. Forcing an early flush does not let you reorder anything.
  • It does honour when. Verified on ansible-core 2.21.3: a meta: flush_handlers with when: false reports skipping and the notification survives to the ordinary end-of-section flush. A skipped flush delays handlers; it does not cancel them.

That last point is worth stating positively, because a skipped flush is easy to misread as a suppressed handler. It is not. The handler still runs — just later, at the section boundary, by which time the task that needed it to have run already ran without it.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Three tasks notify handlers in the order C, A, B. The handlers section defines them in the order A, B, C. In what order do they run?

  2. Q2. A play deploys a config file, notifies a reload handler, then runs a health check task against the service. Why does this play need meta: flush_handlers?

  3. Q3. Which statements about meta: flush_handlers are correct? Select all that apply.

  4. Q4. A handler notified during pre_tasks will run at the end of the play, after post_tasks.

Passing score: 75%. Answers are checked in this browser.