Introduction
Ansible callback plugins customize playbook output and provide additional functionality during execution. The three most useful built-in callbacks — timer, profile_tasks, and profile_roles — help you identify performance bottlenecks by measuring execution time at the playbook, task, and role level. This article covers configuration, all major callback plugins, writing custom callbacks, and using profiling data to optimize playbooks.
Quick Start
Add to your ansible.cfg:
[defaults]
callbacks_enabled = ansible.posix.timer, ansible.posix.profile_tasks, ansible.posix.profile_roles
Install the collection:
ansible-galaxy collection install ansible.posix
Run any playbook — you'll see timing data automatically:
PLAY RECAP *****************************
Wednesday 23 April 2026 14:30:22 +0000 (0:00:01.234) 0:02:15.678 ****
Task timings:
0:00:45.123 — Deploy application
0:00:30.456 — Install packages
0:00:15.789 — Configure nginx
0:00:01.234 — Restart services
Role timings:
0:01:15.579 — webserver
0:00:45.099 — database
0:00:15.000 — monitoring
The Three Profiling Callbacks
ansible.posix.timer
Shows total playbook execution time:
Playbook run took 0 days, 0 hours, 2 minutes, 15 seconds
ansible.posix.profile_tasks
Shows execution time for every task, sorted by duration:
Wednesday 23 April 2026 14:30:22 +0000 (0:00:01.234) 0:02:15.678 ****
Task timings ------
45.12s Deploy application code
30.46s Install system packages
15.79s Configure nginx virtual host
8.23s Run database migrations
1.23s Restart nginx
0.89s Verify service health
ansible.posix.profile_roles
Shows aggregated execution time per role:
Role timings ------
1m15s webserver
45s database
15s monitoring
Configuration Options
ansible.cfg
[defaults]
# Modern syntax (Ansible 2.15+)
callbacks_enabled = ansible.posix.timer, ansible.posix.profile_tasks, ansible.posix.profile_roles
# Legacy syntax (still works)
callback_whitelist = ansible.posix.timer, ansible.posix.profile_tasks, ansible.posix.profile_roles
# Sort profile_tasks output
[callback_profile_tasks]
sort_order = descending # descending (default), ascending, none
task_output_limit = 20 # Show only top 20 slowest tasks
Environment Variables
# Enable callbacks via environment
export ANSIBLE_CALLBACKS_ENABLED="ansible.posix.timer,ansible.posix.profile_tasks"
# Or per-run
ANSIBLE_CALLBACKS_ENABLED=ansible.posix.timer ansible-playbook site.yml
Per-Playbook Override
# You can't enable callbacks per-playbook in YAML
# But you can use environment variables in wrapper scripts:
# profile-run.sh
#!/bin/bash
ANSIBLE_CALLBACKS_ENABLED=ansible.posix.timer,ansible.posix.profile_tasks \
ansible-playbook "$@"
All Built-in Callback Plugins
| Plugin | Type | Description |
|---|---|---|
ansible.posix.timer | aggregate | Total playbook execution time |
ansible.posix.profile_tasks | aggregate | Per-task execution time |
ansible.posix.profile_roles | aggregate | Per-role execution time |
ansible.builtin.default | stdout | Default human-readable output |
ansible.builtin.minimal | stdout | Minimal output |
ansible.builtin.yaml | stdout | YAML-formatted output |
ansible.builtin.json | stdout | JSON-formatted output |
ansible.builtin.dense | stdout | One-line-per-task output |
ansible.builtin.debug | stdout | Debug output with timestamps |
community.general.log_plays | notification | Log to file |
community.general.slack | notification | Send results to Slack |
community.general.logstash | notification | Send to Logstash/ELK |
ara.plugins.callback.default | notification | ARA Records Ansible |
Stdout vs Aggregate vs Notification
- stdout: Only one active at a time (replaces default output)
- aggregate: Multiple can run simultaneously (added to output)
- notification: Multiple can run simultaneously (send data externally)
Changing Output Format
# Use YAML output instead of default
[defaults]
stdout_callback = ansible.builtin.yaml
callbacks_enabled = ansible.posix.timer, ansible.posix.profile_tasks
# Or JSON for machine parsing
[defaults]
stdout_callback = ansible.builtin.json
YAML Output Example
TASK [Install nginx] *****
ok: [web01] =>
changed: false
msg: All items completed
results:
- ansible_loop_var: item
changed: false
item: nginx
msg: nginx is already the newest version
Using Profiling Data to Optimize
Identify Slow Tasks
Task timings ------
45.12s Install packages with apt ← 🐌 Candidate for optimization
30.46s Clone git repository
15.79s Run database migrations
Common Optimizations
# ❌ Slow — installs one package at a time
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis
# ✅ Fast — installs all packages in one transaction
- name: Install packages
ansible.builtin.apt:
name:
- nginx
- postgresql
- redis
state: present
# ❌ Slow — gathers ALL facts
- hosts: all
gather_facts: true
# ✅ Fast — gather only what you need
- hosts: all
gather_facts: false
tasks:
- ansible.builtin.setup:
gather_subset:
- network
- hardware
# ❌ Slow — serial execution
- hosts: webservers
serial: 1
# ✅ Faster — parallel with batches
- hosts: webservers
serial: "30%"
strategy: free
Automation Controller Integration
In AAP/AWX, enable callbacks in the project's ansible.cfg or in Settings → Jobs → Extra Callback Plugins:
# ansible.cfg in your project repo
[defaults]
callbacks_enabled = ansible.posix.timer, ansible.posix.profile_tasks, ansible.posix.profile_roles
The profiling output appears in the job stdout, making it easy to identify slow jobs.
Writing a Custom Callback Plugin
# plugins/callback/custom_timing.py
from ansible.plugins.callback import CallbackBase
import time
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'custom_timing'
def __init__(self):
super().__init__()
self.start_time = None
def v2_playbook_on_start(self, playbook):
self.start_time = time.time()
def v2_playbook_on_stats(self, stats):
elapsed = time.time() - self.start_time
self._display.banner(f"Total time: {elapsed:.1f}s")
# Calculate per-host stats
hosts = sorted(stats.processed.keys())
for host in hosts:
summary = stats.summarize(host)
self._display.display(
f" {host}: ok={summary['ok']} "
f"changed={summary['changed']} "
f"failed={summary['failures']}"
)
Enable it:
[defaults]
callbacks_enabled = custom_timing
callback_plugins = ./plugins/callback
Best Practices
- Always enable profiling in development — catch slow tasks early
- Use
task_output_limit— focus on the top 20 slowest tasks - Profile before optimizing — measure first, then fix
- Disable in production if output verbosity is a concern
- Combine with
strategy: free— profile reveals which tasks benefit from parallel execution - Track over time — compare profiling output across releases to catch regressions
- Use ARA for persistent logging —
ara.plugins.callback.defaultrecords everything to a database
Related Articles
- Ansible Configuration Guide
- Ansible Performance Optimization
- Ansible Automation Platform Guide
- Ansible Filter Plugins
Conclusion
Callback plugins are the easiest way to profile and optimize Ansible playbook performance. Enable timer, profile_tasks, and profile_roles in your ansible.cfg to see exactly where time is spent, then use that data to batch package installs, reduce fact gathering, and parallelize execution. For production monitoring, combine with ARA or Logstash callbacks for persistent performance tracking.