Ansible Handlers: Trigger Tasks on Change (Complete Guide)
By Luca Berton · Published 2026-04-03 · Category: troubleshooting
How to use Ansible handlers for event-driven task execution. Trigger handlers with notify, flush handlers, listen directive, multiple handlers.
Ansible handlers are special tasks that run only when triggered by a notify directive. They're perfect for actions like restarting services — you want them to run only if something actually changed.
Basic Handler Example
- name: Configure nginx
hosts: webservers
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restartedThe handler restart nginx only runs if the template task reports "changed".
See also: Ansible loop_control: Control Loop Output, Labels & Pauses (Guide)
How Handlers Work
- A task with
notifytriggers the handler when the task status is changed - Handlers are queued, not run immediately
- All handlers run once at the end of the play, in the order they're defined
- Even if notified multiple times, a handler runs only once
Multiple Notifications
tasks:
- name: Update nginx.conf
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: Update sites-enabled
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-enabled/mysite.conf
notify: restart nginx
# nginx restarts only ONCE even though notified twice
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restartedNotify Multiple Handlers
tasks:
- name: Update SSL certificate
ansible.builtin.copy:
src: cert.pem
dest: /etc/ssl/certs/app.pem
notify:
- restart nginx
- restart app
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: restart app
ansible.builtin.service:
name: myapp
state: restartedSee also: Ansible Handlers: Trigger Service Restarts on Change (Guide)
listen: Group Handlers
Use listen to trigger multiple handlers with a single notification:
tasks:
- name: Deploy new release
ansible.builtin.unarchive:
src: release.tar.gz
dest: /opt/app
notify: "deploy complete"
handlers:
- name: clear cache
ansible.builtin.command: /opt/app/clear-cache.sh
listen: "deploy complete"
- name: restart app
ansible.builtin.service:
name: myapp
state: restarted
listen: "deploy complete"
- name: notify slack
ansible.builtin.uri:
url: https://hooks.slack.com/services/xxx
method: POST
body_format: json
body:
text: "Deployment complete on {{ inventory_hostname }}"
listen: "deploy complete"flush_handlers: Run Handlers Mid-Play
tasks:
- name: Update config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: restart app
- name: Force handler execution now
ansible.builtin.meta: flush_handlers
- name: Verify app is running (needs restart to complete first)
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200See also: Ansible Troubleshooting: Fix Jinja2 Syntax & Inventory Errors
Handler Execution Order
Handlers run in the order they're defined, not the order they're notified:
handlers:
- name: stop app # Runs first
ansible.builtin.service:
name: myapp
state: stopped
- name: clear cache # Runs second
ansible.builtin.command: rm -rf /tmp/cache
- name: start app # Runs third
ansible.builtin.service:
name: myapp
state: startedHandlers in Roles
roles/
nginx/
tasks/
main.yml
handlers/
main.yml # Handlers auto-loaded from here
templates/
nginx.conf.j2# roles/nginx/handlers/main.yml
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloadedCommon Patterns
Reload vs Restart
tasks:
- name: Update main config (needs restart)
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: Update site config (needs reload only)
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/sites-enabled/mysite.conf
notify: reload nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloadedValidate before restart
handlers:
- name: validate nginx config
ansible.builtin.command: nginx -t
listen: "restart nginx"
- name: restart nginx service
ansible.builtin.service:
name: nginx
state: restarted
listen: "restart nginx"FAQ
Why doesn't my handler run?
The task must report changed status. If the task shows "ok" (no changes), the handler won't trigger. Check with -v for task status.
Can I force a handler to run?
Use changed_when: true on the notifying task, or call meta: flush_handlers followed by the handler task directly.
Do handlers run on failed plays?
No. If a play fails, pending handlers are skipped. Use --force-handlers or set force_handlers = True in ansible.cfg to override.
Can handlers notify other handlers?
Yes! A handler can include notify to chain handlers together.
Basic Handler
- hosts: webservers
become: true
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restartedMultiple Notifiers
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- copy:
src: ssl.conf
dest: /etc/nginx/conf.d/ssl.conf
notify: restart nginx
# Handler runs ONCE even if notified multiple times
handlers:
- name: restart nginx
service: name=nginx state=restartedMultiple Handlers
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- validate nginx config
- restart nginx
handlers:
- name: validate nginx config
command: nginx -t
- name: restart nginx
service: name=nginx state=restartedlisten (Handler Groups)
tasks:
- template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: restart app stack
handlers:
- name: restart app
service: name=myapp state=restarted
listen: restart app stack
- name: restart worker
service: name=myapp-worker state=restarted
listen: restart app stack
- name: clear cache
command: redis-cli FLUSHALL
listen: restart app stackFlush Handlers Mid-Play
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
# Force handler to run NOW (before next task)
- meta: flush_handlers
- name: Verify nginx is running
uri:
url: http://localhost/health
status_code: 200Handler in Roles
# roles/nginx/tasks/main.yml
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
# roles/nginx/handlers/main.yml
- name: restart nginx
service:
name: nginx
state: restarted
become: trueHandler Execution Order
Handlers run:
- At the end of the play (after all tasks)
- In the order defined in the handlers section
- Once regardless of how many tasks notify them
- Only if at least one task notified them and reported
changed
Conditional Handlers
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
when: ansible_os_family == "Debian"changed_when and Handlers
# Handler only fires when task reports changed
- command: /opt/scripts/check-config.sh
register: check
changed_when: "'updated' in check.stdout"
notify: reload configFAQ
Why didn't my handler run?
- The notifying task didn't report
changed(was already in desired state) - The task failed before reaching the handler
- Handler name doesn't match notify string (case-sensitive!)
- Using
--checkmode (handlers don't run in check mode)
Can I force a handler to always run?
Use meta: flush_handlers or move the action to a regular task instead.
Handlers vs regular tasks?
Handlers are conditional — they only run when notified by a changed task. Use handlers for restarts/reloads that should only happen when configuration actually changes.
Basic Handler
tasks:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
become: true
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
become: trueMultiple Notifications
tasks:
- template: { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
notify:
- validate nginx
- restart nginx
handlers:
- name: validate nginx
command: nginx -t
become: true
- name: restart nginx
service: { name: nginx, state: restarted }
become: truelisten (Topic-Based)
tasks:
- template: { src: app.conf.j2, dest: /etc/myapp/config }
notify: app config changed
handlers:
# Multiple handlers respond to one topic
- name: restart app
service: { name: myapp, state: restarted }
listen: app config changed
become: true
- name: clear app cache
file: { path: /tmp/myapp-cache, state: absent }
listen: app config changedflush_handlers
tasks:
- template: { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
notify: restart nginx
# Force handlers to run NOW (before continuing)
- meta: flush_handlers
- uri:
url: http://localhost/health
status_code: 200
retries: 5
delay: 2Handler Ordering
# Handlers run in DEFINITION order, not notification order
handlers:
- name: reload systemd # Runs 1st (defined first)
systemd: { daemon_reload: true }
- name: restart app # Runs 2nd
service: { name: myapp, state: restarted }
- name: check health # Runs 3rd
uri: { url: "http://localhost/health" }Handler in Roles
# roles/nginx/handlers/main.yml
- name: restart nginx
service: { name: nginx, state: restarted }
become: true
- name: reload nginx
service: { name: nginx, state: reloaded }
become: true
# roles/nginx/tasks/main.yml
- template: { src: nginx.conf.j2, dest: /etc/nginx/nginx.conf }
notify: reload nginxConditional Handler
handlers:
- name: restart nginx
service: { name: nginx, state: restarted }
become: true
when: ansible_os_family == "Debian"Handler with Block
tasks:
- block:
- template: { src: a.conf.j2, dest: /etc/a.conf }
- template: { src: b.conf.j2, dest: /etc/b.conf }
notify: restart app
# Handler notified if ANY task in block changesHandlers Run Once
# Even if notify is called 5 times, handler runs ONCE
- template: { src: "{{ item }}.j2", dest: "/etc/conf.d/{{ item }}" }
loop: [a, b, c, d, e]
notify: restart app
# restart app runs ONCE at end, not 5 timesFAQ
When do handlers actually run?
At the end of the play (after all tasks), or when meta: flush_handlers is called.
What if the play fails before handlers run?
Notified handlers do NOT run if the play fails. Use --force-handlers or force_handlers: true in the play to override.
Can handlers notify other handlers?
Yes — a handler can include notify: to chain to another handler.
Related Articles
Category: troubleshooting