AnsibleXVI · HandlersHandlers
Naming handlers, and role collisions
What you'll learn
- Explain why a handler name cannot contain a host-specific variable
- Read the warning Ansible emits when a handler name fails to template
- Predict what happens when two roles define the same handler name
- Design handler names and topics that survive a role being added to a play
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
Handler names look like ordinary task names. They are not: they are lookup keys, and they are resolved when the play is assembled — before any host has been contacted and before host-specific variables exist.
That single implementation fact produces two distinct production failures. The first is a handler that vanishes when you add a variable to its name. The second is two roles quietly sharing one handler.
Names are resolved early, so host variables are not available
A play-level variable in a handler name works, because it is known at assembly time:
$ ansible-playbook -i inv.ini hname.ymlTASK [Notify a templated handler name] *****************************************
changed: [localhost]
RUNNING HANDLER [Restart nginx] ************************************************
ok: [localhost]Move the same variable into the inventory, so each host has its own value, and the handler stops existing:
[web]
alpha.example.com ansible_connection=local svc=nginx
beta.example.com ansible_connection=local svc=haproxy$ ansible-playbook -i inv2.ini hname2.yml[WARNING]: Handler 'Restart {{ svc }}' is unusable because it has no listen
topics and the name could not be templated (host-specific variables are not
supported in handler names). The error: 'svc' is undefined
[ERROR]: The requested handler 'Restart nginx' was not found in either the main
handlers list nor in the listening handlers listRead the warning carefully, because it is unusually informative and it explains the whole design:
- “host-specific variables are not supported in handler names” — stated outright. Resolution happens once, for the play, not once per host.
- “it has no listen topics” — a handler whose name fails to
template is still reachable through a
listentopic, because topics are static strings. The handler is not discarded; it is only unreachable by name. - “The error: ‘svc’ is undefined” — at assembly time,
svcdoes not exist, because host variables have not been resolved for any host yet.
Note also that the run got as far as printing Restart nginx in the
error. The notify on the task was templated, per host, successfully.
The handler name was not. The two sides of the lookup are resolved at
different times, which is exactly why they fail to meet.
Two roles, one handler name
Handlers from roles are added to the play’s handler list. There is no per-role namespace, no prefixing, and no collision detection.
So a play that includes a webserver role and a monitoring role, both
of which define a handler called Restart the service — a plausible
name for either — has a problem.
- hosts: local
gather_facts: false
tasks:
- name: Notify the colliding name
ansible.builtin.debug:
msg: t
changed_when: true
notify: Restart the service
handlers:
- name: Restart the service
ansible.builtin.debug:
msg: FIRST DEFINITION ran
- name: Restart the service
ansible.builtin.debug:
msg: SECOND DEFINITION ran$ ansible-playbook -i inv.ini dup.ymlTASK [Notify the colliding name] ***********************************************
changed: [localhost]
RUNNING HANDLER [Restart the service] ******************************************
ok: [localhost] => {
"msg": "FIRST DEFINITION ran"
}
PLAY RECAP *********************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0One handler ran. The other did not, and nothing said so.
Now map that onto the roles. The monitoring role’s task changes its
configuration and notifies Restart the service, intending its own
handler. What actually happens depends on which role was listed first in
the play. If webserver came first, the monitoring change restarts
nginx, and the monitoring agent is never restarted at all.
Naming that survives composition
Three rules, in order of how much they buy you.
1. Name the handler for its own service. Restart the service is a
name that collides with every other role in the repository. Restart the monitoring agent does not. This alone prevents most collisions, costs
nothing, and makes the run log readable.
2. Publish a listen topic and let external callers use it. A role
should be notified through a topic it declares, not through a handler
name it happens to have. That is the contract argument from the previous
lesson, and it also removes the collision risk entirely, because two
roles subscribing to two differently-named topics never meet.
3. Read the assembled handler list when composing a play. For a play
built from roles you did not all write, the handler namespace is a
property of the play, not of any role. There is no command that prints
it directly, but the roles’ handlers/main.yml files are short and
reading them is a two-minute job that a composition bug costs an
afternoon.
grep -rhE '^[[:space:]]*- name:' roles/*/handlers/ \
| sed 's/^[[:space:]]*- name:[[:space:]]*//' \
| sort | uniq -c | sort -rn | head -20Any count above 1 in that output is a name defined in two roles. It is
not automatically a bug — the roles may never appear in the same play —
but it is the list worth checking against the plays you actually run.
Knowledge check
Knowledge check · 4 questions
Q1. A handler is named "Restart {{ svc }}" and svc is set per host in the inventory. What happens?
Q2. A play includes a webserver role and a monitoring role. Both define a handler named "Restart the service". The monitoring role changes its config and notifies that name. What happens?
Q3. Which of these correctly avoid the templated-handler-name problem? Select all that apply.
Q4. When two handlers share a name, Ansible emits a warning so the collision is visible in the run output.
Passing score: 75%. Answers are checked in this browser.