Introduction
The throttle keyword in Ansible limits how many hosts can execute a specific task at the same time, regardless of the forks setting. While forks controls the global parallelism and serial controls batch size for plays, throttle gives per-task concurrency control. Use it to prevent overloading a shared resource like a database, API endpoint, load balancer, or license server during automation.
Syntax
- name: Task with throttle
ansible.builtin.command: do-something
throttle: 3 # Only 3 hosts run this task simultaneously
throttle vs serial vs forks
Setting | Scope | Controls | Set Where
─────────|───────────|─────────────────────────────────|──────────────
forks | Global | Max parallel host connections | ansible.cfg
serial | Play | Batch size (hosts per wave) | Play level
throttle | Task | Max concurrent for ONE task | Task level
# Example: 100 hosts, forks=50, serial=20, throttle=3 on one task
#
# Play runs in batches of 20 hosts (serial=20)
# Within each batch, up to 50 connections (forks=50)
# But the throttled task → max 3 hosts at a time
# All other tasks run at full speed (up to 50 parallel)
Basic Examples
Rate-Limit API Calls
---
- name: Update monitoring agents
hosts: all_servers
tasks:
- name: Download agent (unlimited parallel)
ansible.builtin.get_url:
url: "https://monitoring.example.com/agent.tar.gz"
dest: /tmp/agent.tar.gz
- name: Register with monitoring API (rate-limited)
ansible.builtin.uri:
url: "https://api.monitoring.example.com/register"
method: POST
body_format: json
body:
hostname: "{{ inventory_hostname }}"
ip: "{{ ansible_default_ipv4.address }}"
throttle: 5 # API can handle max 5 concurrent requests
Rolling Service Restart
---
- name: Rolling restart of web servers
hosts: webservers
tasks:
- name: Deploy new config (parallel is fine)
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
- name: Remove from load balancer
ansible.builtin.uri:
url: "https://lb.example.com/api/pool/remove"
method: POST
body_format: json
body:
server: "{{ inventory_hostname }}"
throttle: 1 # One at a time to maintain capacity
- name: Restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
throttle: 1 # Match the LB removal
- name: Wait for nginx to be healthy
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}/health"
status_code: 200
register: health
until: health.status == 200
retries: 10
delay: 5
throttle: 1
- name: Add back to load balancer
ansible.builtin.uri:
url: "https://lb.example.com/api/pool/add"
method: POST
body_format: json
body:
server: "{{ inventory_hostname }}"
throttle: 1
Database Migration
---
- name: Run database migrations
hosts: app_servers
tasks:
- name: Enable maintenance mode
ansible.builtin.copy:
content: "maintenance"
dest: /var/www/app/.maintenance
- name: Run database migration (only one server at a time!)
ansible.builtin.command: /var/www/app/artisan migrate --force
throttle: 1 # Only ONE server should run migrations
run_once: false # Each server verifies its connection
register: migration
- name: Disable maintenance mode
ansible.builtin.file:
path: /var/www/app/.maintenance
state: absent
License Server Protection
- name: Run licensed software scan
ansible.builtin.command: /opt/scanner/run --full
throttle: 10 # License server allows max 10 concurrent
async: 3600 # Run up to 1 hour
poll: 30 # Check every 30 seconds
throttle with Blocks
---
- name: Batch operations with throttle on block
hosts: database_replicas
tasks:
- name: Failover operations (one at a time)
throttle: 1
block:
- name: Stop replication
ansible.builtin.command: mysql -e "STOP REPLICA"
- name: Apply schema change
ansible.builtin.command: mysql < /tmp/schema-update.sql
- name: Start replication
ansible.builtin.command: mysql -e "START REPLICA"
- name: Verify replication status
ansible.builtin.command: mysql -e "SHOW REPLICA STATUS\G"
register: replica_status
until: "'Slave_IO_Running: Yes' in replica_status.stdout"
retries: 30
delay: 10
rescue:
- name: Alert on failure
ansible.builtin.debug:
msg: "CRITICAL: Replication fix needed on {{ inventory_hostname }}"
Common Mistakes
# ❌ MISTAKE 1: throttle > forks (no effect)
# If forks=5 and throttle=10, only 5 can run anyway
# throttle only REDUCES parallelism, never increases it
# ❌ MISTAKE 2: Using throttle when serial is better
# If ALL tasks in a play need limiting → use serial at play level
# throttle is for when only SOME tasks need limiting
# ❌ MISTAKE 3: throttle: 0 (invalid)
# Minimum value is 1 — use it for strict serialization
# ❌ MISTAKE 4: Forgetting throttle on related tasks
# If you throttle the "remove from LB" but not the "restart",
# restarts can still happen in parallel
- name: Remove from LB
throttle: 1 # ✅
- name: Restart service
# ❌ Missing throttle — restarts happen at full parallelism!
ansible.builtin.systemd:
name: nginx
state: restarted
# FIX: Use a throttled block for related tasks
# ❌ MISTAKE 5: Using throttle for global rate limiting
# throttle is per-task. If you have 5 tasks each with throttle: 3,
# you can have up to 3 × 5 = 15 connections to the same resource
# For global limiting, use serial at the play level
Related Articles
- Ansible Strategies — linear, free, debug
- Ansible async and poll
- Ansible Playbook Best Practices
- Ansible Error Handling
- Ansible At Scale Performance Tuning
Conclusion
Use throttle when a specific task needs rate-limiting but the rest of the play can run at full parallelism. Set throttle: 1 for operations that must be strictly sequential (database migrations, load balancer drain), throttle: N for resources with a known concurrency limit (APIs, license servers), and combine with block to throttle a group of related tasks together. It's the surgical tool for concurrency control — more precise than serial, more targeted than reducing forks.