Skip to content

Handlers Patterns


Let’s see some of the real patterns that are used and solved with handlers. This combines everything we have been learning about handlers and other features of Ansible.

Pattern: Alerting From A rescue Block

rescue: isn’t limited to recovery logic — it can notify a completely different handler than the happy path would have, turning a failure into a genuine alert:

alert_pattern.yaml
- name: Resilient Deployment With Alerting
  hosts: servers
  tasks:
    - name: Attempt deployment
      block:
        - name: Deploy configuration
          ansible.builtin.command: /bin/false
          register: deploy_result
          notify: Restart app

      rescue:
        - name: Notify the alert handler
          ansible.builtin.debug:
            msg: "Preparing alert"
          changed_when: true
          notify: Send alert

  handlers:
    - name: Restart app
      ansible.builtin.debug:
        msg: "Restarting the app"

    - name: Send alert
      ansible.builtin.debug:
        msg: "ALERT: deployment failed on {{ inventory_hostname }}"
ok: [ubuntu] => { "msg": "ALERT: deployment failed on ubuntu" }

The deployment fails, so Restart app is never notified at all — a failed task doesn’t report changed, so Rule One from the handlers chapter keeps it from ever queuing. rescue: catches the failure and notifies Send alert instead — a genuinely different handler, reached only via the failure path.

force_handlers: Making Sure Pending Handlers Still Fire

Here’s a real, dangerous gap in the default behavior. If a handler is already queued, and a later, unrelated task in the same play then fails, the play stops — and the already-queued handler never runs at all, since handlers are deferred to the play’s natural end, which the play never actually reaches.

risky_default.yaml
- name: Default Behavior (risky)
  hosts: servers
  tasks:
    - name: Deploy configuration
      ansible.builtin.copy:
        src: app.conf
        dest: /etc/app.conf
      notify: Restart app

    - name: A later, unrelated task that fails
      ansible.builtin.command: /bin/false

  handlers:
    - name: Restart app
      ansible.builtin.debug:
        msg: "Restarting the app now"
changed: [ubuntu]
fatal: [ubuntu]: FAILED! => {"msg": "non-zero return code"}

Restart app never prints at all — the new config was deployed, but the play failed before ever reaching the point where queued handlers run, leaving a genuinely worse state than either fully succeeding or fully failing: a new config file sitting on disk, with the old process still running and never told to reload it.

force_handlers: true fixes this directly — queued handlers still run, even when a later task fails:

fixed_force_handlers.yaml
- name: Fixed With force_handlers
  hosts: servers
  force_handlers: true
  tasks:
    - name: Deploy configuration
      ansible.builtin.copy:
        src: app.conf
        dest: /etc/app.conf
      notify: Restart app

    - name: A later, unrelated task that fails
      ansible.builtin.command: /bin/false

  handlers:
    - name: Restart app
      ansible.builtin.debug:
        msg: "Restarting the app now"
changed: [ubuntu]
ok: [ubuntu] => { "msg": "Restarting the app now" }
fatal: [ubuntu]: FAILED! => {"msg": "non-zero return code"}

The play still ultimately fails — force_handlers doesn’t paper over the real failure — but Restart app runs regardless, keeping the config and the running process in sync even though something else genuinely went wrong.

Pattern: Deploy, Verify, Alert On Failure — All Together

Everything from this entire section, combined into one realistic deployment flow:

full_pattern.yaml
- name: Full Deployment Pattern
  hosts: servers
  force_handlers: true
  tasks:
    - name: Deployment attempt
      block:
        - name: Deploy configuration
          ansible.builtin.copy:
            src: app.conf
            dest: /etc/app.conf
          notify: Restart app

        - name: Force restart to happen now
          ansible.builtin.meta: flush_handlers

        - name: Verify app responds after restart
          ansible.builtin.command: /bin/false
          register: verify_result

      rescue:
        - name: Alert on any failure in this deployment
          ansible.builtin.debug:
            msg: "Deployment or verification failed on {{ inventory_hostname }}"
          changed_when: true
          notify: Send alert

  handlers:
    - name: Restart app
      ansible.builtin.debug:
        msg: "Restarting the app"

    - name: Send alert
      ansible.builtin.debug:
        msg: "ALERT: something failed on {{ inventory_hostname }}"

Deploy, force the restart to actually happen before verification even starts, verify, and if anything at all in that sequence fails, rescue: catches it and alerts distinctly — with force_handlers standing guard in case some other, unrelated task elsewhere in the play fails too.

What You Just Did

block, rescue, notify, handler ordering, listen:, flush_handlers, and now force_handlers — six chapters’ worth of tools, all present in that one pattern above. And reaching further back still: inventory_hostname (the variables section’s magic variables), copy’s genuine idempotency (all the way back in the very first playbook chapter), and register/is changed (tests and conditionals) all show up too, doing exactly the jobs they were introduced to do. Nothing here is new mechanics — it’s composition, the same way the tests and conditionals section’s closing chapter was.

Best Practices

  • Notify a dedicated alert handler from within rescue: blocks, separate from your normal success-path handlers — a genuine failure deserves a genuinely different response, not silence or a generic restart.
  • Set force_handlers: true for any deployment-style playbook where a queued handler’s effect must happen regardless of what else in the play fails — leaving a change applied without its corresponding follow-up action is a worse outcome than either a clean success or a clean failure.
  • Combine flush_handlers, rescue, and force_handlers deliberately when building genuinely resilient playbooks — each solves a different specific gap, and real reliability comes from knowing which one closes which gap, not from reaching for all of them out of habit.

That closes out this section. block and rescue for grouping and recovering from failure, handlers and their two core rules — including the loop-notification trip-up specifically flagged at the start of this section — execution ordering and listen:, and now force_handlers closing a real operational gap. The next section covers lookups: pulling data in from outside a playbook entirely, including the fileglob pattern this section’s with_X migration chapter already previewed.

Last updated on