AnsibleXVI · HandlersHandlers
Handler ordering and flush_handlers
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
Two facts about handler execution cause most of the confusion in this part. Neither is obscure, and neither is guessable:
- Handlers do not run where they were notified. They run at a flush point, and a play has three of them by default.
- Handlers do not run in the order they were notified. They run in the
order they were defined in the
handlerssection.
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.
$ ansible-playbook -i inv.ini flush.ymlTASK [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.
- 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$ ansible-playbook -i inv.ini order.ymlTASK [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=0Notified 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.
- 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: reloadedThis 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 onansible-core2.21.3: ameta: flush_handlerswithwhen: falsereportsskippingand 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
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?
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?
Q3. Which statements about meta: flush_handlers are correct? Select all that apply.
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.