The Unnecessary Restart Problem
Consider a playbook that deploys an updated nginx configuration file. After the file is deployed, nginx must be reloaded to pick up the changes. The naive approach is to add a "Reload nginx" task at the end of the playbook. But this task will run on every playbook execution — even when the config file hasn't changed — causing unnecessary service disruptions.
Handlers solve this elegantly. A handler is a special task that only executes when explicitly notified by another task that reported a changed result. If the notifying task reports ok (no change), the handler is never triggered. Multiple tasks — deploying a config file, deploying an SSL certificate — can both notify the same "Reload nginx" handler.
Handler Execution Timing
Handlers do not run immediately when notified. They are collected throughout the play and run once at the end of the play, regardless of how many tasks notified them. This means if both the config file task and the SSL certificate task report changed, the "Reload nginx" handler still only runs once — not twice.
This batching behavior is intentional and prevents redundant restarts. If a handler genuinely needs to run mid-play, before the play ends — for example, a config change needs to take effect before a later task that depends on the new config — a dedicated meta-task can force pending handlers to run immediately rather than waiting for the end of the play.
Deploying config files this way also pairs naturally with a validation step: running a syntax check against the new file before it's deployed. If validation fails, the file is not deployed and the handler is never notified — preventing a broken config from ever reaching the server.