Handler Execution Order
By default, handlers run at the end of a play. This chapter covers exactly what order they run in when there’s more than one — and it’s not the order you’d probably guess.
The Trap: Definition Order, Not Notification Order
Handlers run in the order they’re defined in the handlers: list — not the order they were notified during task execution. This genuinely surprises almost everyone the first time it matters.
- name: Order Trap Demo
hosts: servers
tasks:
- name: Notify B first
ansible.builtin.debug:
msg: "notifying B"
changed_when: true
notify: Handler B
- name: Notify A second
ansible.builtin.debug:
msg: "notifying A"
changed_when: true
notify: Handler A
handlers:
- name: Handler A
ansible.builtin.debug:
msg: "Running A"
- name: Handler B
ansible.builtin.debug:
msg: "Running B"Note
changed_when: true forces a task to report changed regardless of what it actually did — needed here since debug never genuinely changes anything on its own, and Rule One from the previous chapter means an actually-unchanged task wouldn’t notify its handler at all. This is just for simulation. In real world, you will have task that changes.
TASK [Notify B first] *****************
changed: [ubuntu] => {
"msg": "notifying B"
}
TASK [Notify A second] ****************
changed: [ubuntu] => {
"msg": "notifying A"
}
RUNNING HANDLER [Handler A] ***********
ok: [ubuntu] => {
"msg": "Running A"
}
RUNNING HANDLER [Handler B] ***********
ok: [ubuntu] => {
"msg": "Running B"
}Handler B was notified first, during task execution — and yet Handler A runs first regardless, because A is listed before B in the handlers: section. Notification order is completely irrelevant to execution order; only where a handler sits in its own list matters.
listen: — Notifying A Topic Instead Of A Specific Handler
Sometimes one conceptual event should trigger several distinct handlers at once — restarting an app and its reverse proxy together, say. listen: lets multiple handlers subscribe to a shared topic name, and a single notify: can target that topic instead of naming each handler individually:
- name: Listen Demo
hosts: servers
tasks:
- name: Deploy config
ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
notify: "restart services"
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Restarting app"
listen: "restart services"
- name: Restart proxy
ansible.builtin.debug:
msg: "Restarting proxy"
listen: "restart services"ok: [ubuntu] => { "msg": "Restarting app" }
ok: [ubuntu] => { "msg": "Restarting proxy" }One notify:, both handlers fired — since both listen: to the same topic. The definition-order rule from earlier still applies here too: among handlers sharing a topic, they still run in the order they’re defined, and each one is still individually subject to Rule Two — running at most once per play, regardless of how many things notified the shared topic.
meta: flush_handlers — Running Handlers Early
Handlers waiting until the very end of the play is a problem the moment a later task in the same play genuinely needs a handler’s effect to have already happened — verifying a service is actually running again after a restart handler was supposed to fire, for instance. meta: flush_handlers is a special pseudo-task that forces every currently-queued handler to run immediately, right there in the task list, rather than waiting:
- name: Flush Handlers Demo
hosts: servers
tasks:
- name: Deploy configuration
ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
notify: Restart app
- name: Force pending handlers to run now
ansible.builtin.meta: flush_handlers
- name: Verify app is responding after restart
ansible.builtin.debug:
msg: "Checking the app, now that the handler has definitely already run"
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Restarting the app now"Without the flush_handlers step, Verify app is responding would run before Restart app ever fired — handlers deferred to the play’s end simply wouldn’t have happened yet. With it inserted between the two, the restart is guaranteed complete by the time verification runs.
A Realistic Example: Restart Before Verifying
Combining both this chapter’s tools into one coherent flow — deploy, force the restart to happen now via a shared topic, then verify:
tasks:
- name: Deploy app config
ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
notify: "restart services"
- name: Deploy proxy config
ansible.builtin.copy:
src: proxy.conf
dest: /etc/proxy.conf
notify: "restart services"
- name: Force restarts to happen now
ansible.builtin.meta: flush_handlers
- name: Verify everything is back up
ansible.builtin.debug:
msg: "Both services should be freshly restarted by now"
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Restarting app"
listen: "restart services"
- name: Restart proxy
ansible.builtin.debug:
msg: "Restarting proxy"
listen: "restart services"Best Practices
- Never assume handler execution order matches notification order — check the
handlers:list’s own sequence any time the relative order between two handlers actually matters. - Use
listen:when several distinct handlers should always fire together for one conceptual event, rather than notifying each one individually from every relevant task. - Use
meta: flush_handlerswhen a later task in the same play genuinely depends on a handler’s effect having already happened — don’t assume “it’ll run eventually” is good enough if something afterward needs it to have run already.