Skip to content

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.

order_trap.yaml
- 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:

listen_demo.yaml
- 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:

flush_demo.yaml
- 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_handlers when 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.
Last updated on