Introduction
Ansible's run_once directive is a deceptively simple feature that can dramatically improve playbook efficiency. While it appears straightforward — "run this task only once" — its interaction with variables, delegation, strategies, and serial execution makes it one of Ansible's most powerful optimization tools.
This guide covers everything you need to know about run_once, from basic usage to advanced patterns that can cut your playbook execution time significantly.
What Does run_once Do?
When you add run_once: true to a task, Ansible executes that task on only one host (the first host in the current batch) instead of running it on every host in the play. The key insight is that results — including registered variables — are still shared with all hosts.
- name: Simple run_once example
hosts: all
tasks:
- name: Get the current date
ansible.builtin.command: date
run_once: true
register: current_date
- name: Show the date on all hosts
ansible.builtin.debug:
var: current_date.stdout
Even though the date command runs on only one host, every host in the play can access current_date.stdout.
Play Level vs Task Level
Play Level
At the play level, run_once is equivalent to targeting only the first host:
- name: Play level run_once
hosts: host1,host2,host3,host4,host5
run_once: true
tasks:
- name: Print message
ansible.builtin.debug:
msg: "Hello World"
This is functionally the same as hosts: host1 or hosts: all[0]. It works, but isn't particularly exciting.
Task Level
The real power of run_once shines at the task level, where you can mix single-execution and multi-host tasks in the same play:
- name: Deploy application
hosts: web_servers
tasks:
- name: Download release artifact (once)
ansible.builtin.get_url:
url: "https://releases.example.com/app-{{ version }}.tar.gz"
dest: /tmp/app-release.tar.gz
run_once: true
delegate_to: localhost
- name: Copy to all servers
ansible.builtin.copy:
src: /tmp/app-release.tar.gz
dest: /opt/app/release.tar.gz
- name: Extract and deploy
ansible.builtin.unarchive:
src: /opt/app/release.tar.gz
dest: /opt/app/
remote_src: true
The download happens once, but the copy and deploy run on every host.
run_once with register
One of run_once's best features is how it interacts with register. The registered variable is created for all hosts in the play, not just the host that executed the task:
- name: Database migration
hosts: app_servers
tasks:
- name: Run database migration (only once)
ansible.builtin.command: /opt/app/migrate.sh
run_once: true
register: migration_result
- name: Show migration status on all hosts
ansible.builtin.debug:
msg: "Migration status: {{ migration_result.rc }}"
This behavior is unlike when combined with register, where skipped hosts get an "undefined" or "skipped" result. With run_once, the variable is fully populated on all hosts.
Pseudo-Facts Pattern
The pseudo-facts pattern uses run_once with set_fact to set a variable once that all hosts can use:
- name: Pseudo-facts example
hosts: all
tasks:
- name: Get configuration version from API
ansible.builtin.uri:
url: https://config-api.example.com/version
return_content: true
run_once: true
register: config_api
- name: Set config version fact
ansible.builtin.set_fact:
config_version: "{{ config_api.json.version }}"
run_once: true
- name: Deploy configuration for this version
ansible.builtin.template:
src: "config-{{ config_version }}.j2"
dest: /etc/app/config.yml
Without run_once, the API call would execute for every host — wasteful if the result is identical. With run_once, one API call serves the entire play.
run_once with delegate_to
Combining run_once with delegate_to is extremely powerful for tasks that should happen on a specific host:
- name: Cluster maintenance
hosts: cluster_nodes
tasks:
- name: Disable cluster health checks
ansible.builtin.uri:
url: "http://monitor.example.com/api/maintenance"
method: POST
body: '{"cluster": "production", "enabled": true}'
body_format: json
run_once: true
delegate_to: localhost
- name: Perform rolling update
ansible.builtin.yum:
name: myapp
state: latest
- name: Re-enable health checks
ansible.builtin.uri:
url: "http://monitor.example.com/api/maintenance"
method: POST
body: '{"cluster": "production", "enabled": false}'
body_format: json
run_once: true
delegate_to: localhost
run_once with serial
When using serial for batched execution, run_once executes once per batch, not once for the entire play:
- name: Rolling deployment
hosts: web_servers
serial: 3
tasks:
- name: Log batch start
ansible.builtin.debug:
msg: "Starting new batch"
run_once: true # Runs once per batch of 3
- name: Update application
ansible.builtin.yum:
name: webapp
state: latest
If you have 9 hosts with serial: 3, the "Log batch start" task runs 3 times (once per batch).
Strategy Considerations
Linear Strategy (Default)
With the default linear strategy, run_once behaves predictably — the first host in the batch executes the task, and all hosts get the result.
Free Strategy
With strategy: free, hosts execute tasks independently and at their own pace. This means run_once may behave differently:
- name: Free strategy example
hosts: all
strategy: free
tasks:
- name: Set shared fact
ansible.builtin.set_fact:
shared_value: "computed_result"
run_once: true
With strategy: free, there's no guarantee which host runs first or that the run_once result is available to other hosts before they need it. Avoid run_once with strategy: free unless the task has no cross-host dependencies.
Common Use Cases
One-Time Database Operations
- name: Run database schema update once
ansible.builtin.command: /opt/app/bin/db-migrate
run_once: true
delegate_to: "{{ groups['db_primary'][0] }}"
register: db_migrate
Sending Notifications
- name: Notify Slack about deployment
community.general.slack:
token: "{{ slack_token }}"
channel: "#deployments"
msg: "Deploying {{ app_version }} to {{ ansible_play_hosts | length }} hosts"
run_once: true
delegate_to: localhost
Gathering External Data
- name: Fetch latest AMI ID
amazon.aws.ec2_ami_info:
filters:
name: "myapp-*"
state: available
sort: creation_date
sort_order: descending
sort_end: 1
run_once: true
delegate_to: localhost
register: latest_ami
Creating Shared Resources
- name: Create S3 deployment bucket
amazon.aws.s3_bucket:
name: "{{ deploy_bucket }}"
state: present
run_once: true
delegate_to: localhost
Best Practices
- Use for identical operations —
run_onceis ideal when the result would be the same regardless of which host runs it - Combine with
delegate_to— For API calls, database operations, or cloud resource management - Avoid with
strategy: free— Results may not propagate predictably - Remember serial batching —
run_oncemeans once per batch, not once per play - Use for expensive operations — API calls, downloads, and database queries benefit most from
run_once - Document the intent — Add comments explaining why
run_onceis used to help future maintainers
Related Articles
- Mastering Dynamic Variable Creation with set_fact
- Ansible Best Practices Guide
- 10 Ways to Speed Up Your Ansible Playbooks
- Ansible Error Handling Guide
- Ansible Dry Run: Check and Diff Mode
- Ansible Debug Module Guide
- Three Options to Limit Playbook Execution
Conclusion
The run_once directive is one of Ansible's most effective optimization tools. It reduces redundant operations, simplifies playbook logic, and ensures consistency when tasks need to execute only once across a fleet of hosts.
Key takeaways:
- Use
run_onceat the task level for maximum flexibility - Registered variables propagate to all hosts automatically
- Combine with
delegate_tofor centralized operations like API calls - Be aware of
serialbatching behavior —run_oncemeans once per batch - Avoid mixing
run_oncewithstrategy: free