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

PluginTypeDescription
ansible.posix.timeraggregateTotal playbook execution time
ansible.posix.profile_tasksaggregatePer-task execution time
ansible.posix.profile_rolesaggregatePer-role execution time
ansible.builtin.defaultstdoutDefault human-readable output
ansible.builtin.minimalstdoutMinimal output
ansible.builtin.yamlstdoutYAML-formatted output
ansible.builtin.jsonstdoutJSON-formatted output
ansible.builtin.densestdoutOne-line-per-task output
ansible.builtin.debugstdoutDebug output with timestamps
community.general.log_playsnotificationLog to file
community.general.slacknotificationSend results to Slack
community.general.logstashnotificationSend to Logstash/ELK
ara.plugins.callback.defaultnotificationARA 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

  1. Always enable profiling in development — catch slow tasks early
  2. Use task_output_limit — focus on the top 20 slowest tasks
  3. Profile before optimizing — measure first, then fix
  4. Disable in production if output verbosity is a concern
  5. Combine with strategy: free — profile reveals which tasks benefit from parallel execution
  6. Track over time — compare profiling output across releases to catch regressions
  7. Use ARA for persistent logging — ara.plugins.callback.default records everything to a database

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.