Introduction
run_once: true tells Ansible to execute a task on only one host in the play, even when the play targets multiple hosts. It's essential for one-time operations like database migrations, load balancer updates, or sending a single notification — tasks that should happen exactly once, not once per host.
Basic Syntax
- name: Run database migration (once only)
ansible.builtin.command: /opt/app/migrate.sh
run_once: true
By default, Ansible runs the task on the first host in the inventory for that play. All other hosts skip it. The registered variable is still available on all hosts.
How run_once Works
---
- name: Deploy application
hosts: webservers # web-01, web-02, web-03
tasks:
- name: This runs on ALL hosts
ansible.builtin.debug:
msg: "Running on {{ inventory_hostname }}"
# Output: web-01, web-02, web-03
- name: This runs on FIRST host only
ansible.builtin.debug:
msg: "Running on {{ inventory_hostname }}"
run_once: true
# Output: web-01 only
- name: But the result is available everywhere
ansible.builtin.command: cat /opt/app/version.txt
run_once: true
register: app_version
- name: All hosts can use the registered variable
ansible.builtin.debug:
msg: "Version is {{ app_version.stdout }}"
# Output: all three hosts print the version
run_once with delegate_to
Combine run_once with delegate_to to run the task on a specific host:
---
- name: Deploy with database migration
hosts: webservers
tasks:
- name: Run migration on the database server (not a webserver)
ansible.builtin.command: /opt/app/migrate.sh
run_once: true
delegate_to: "{{ groups['dbservers'][0] }}"
- name: Update load balancer (once, on the LB)
ansible.builtin.uri:
url: "https://{{ groups['loadbalancers'][0] }}/api/backends"
method: POST
body_format: json
body:
servers: "{{ groups['webservers'] }}"
run_once: true
delegate_to: localhost
- name: Send Slack notification (once, from control node)
ansible.builtin.uri:
url: "{{ slack_webhook }}"
method: POST
body_format: json
body:
text: "Deployment complete to {{ ansible_play_hosts | length }} servers"
run_once: true
delegate_to: localhost
⚠️ run_once with strategy: free
This is the most common pitfall. The warning message:
[WARNING]: Using run_once may behave differently if strategy is set to free
With strategy: linear (default), all hosts reach each task together, so run_once reliably executes on the first host and skips the rest.
With strategy: free, hosts run independently at their own pace. The first host to reach a run_once task executes it — but other hosts might reach it before the first host finishes, causing unpredictable behavior.
---
# SAFE — linear strategy (default)
- name: Deploy app
hosts: webservers
strategy: linear
tasks:
- name: Migrate database
ansible.builtin.command: /opt/app/migrate.sh
run_once: true # Predictable — runs on first host
---
# RISKY — free strategy
- name: Deploy app
hosts: webservers
strategy: free
tasks:
- name: Migrate database
ansible.builtin.command: /opt/app/migrate.sh
run_once: true # Which host runs first? Unpredictable!
Fix: If you need strategy: free with one-time tasks, move the run_once task to a separate play with strategy: linear:
---
- name: Pre-deployment (linear, safe for run_once)
hosts: webservers
strategy: linear
tasks:
- name: Migrate database
ansible.builtin.command: /opt/app/migrate.sh
run_once: true
- name: Deploy application (free, parallel)
hosts: webservers
strategy: free
tasks:
- name: Pull latest code
ansible.builtin.git:
repo: "{{ app_repo }}"
dest: /opt/app
run_once with serial
When using serial (rolling updates), run_once executes once per batch, not once for the entire play:
---
- name: Rolling update
hosts: webservers # 6 hosts
serial: 2 # 3 batches of 2
tasks:
- name: This runs 3 times (once per batch)!
ansible.builtin.debug:
msg: "Batch task on {{ inventory_hostname }}"
run_once: true
# Runs on: web-01 (batch 1), web-03 (batch 2), web-05 (batch 3)
If you truly need once for the entire play, use a separate play:
---
- name: One-time setup
hosts: webservers[0] # Target only first host
tasks:
- name: Runs exactly once
ansible.builtin.command: /opt/app/setup.sh
- name: Rolling deploy
hosts: webservers
serial: 2
tasks:
- name: Deploy application
ansible.builtin.copy:
src: app/
dest: /opt/app/
Common Patterns
Database Migration Before Deploy
---
- name: Deploy with migration
hosts: webservers
tasks:
- name: Stop application
ansible.builtin.service:
name: myapp
state: stopped
- name: Run database migration
ansible.builtin.command:
cmd: python manage.py migrate --noinput
chdir: /opt/app
run_once: true
register: migration
- name: Show migration output
ansible.builtin.debug:
msg: "{{ migration.stdout_lines }}"
run_once: true
- name: Deploy new code
ansible.builtin.copy:
src: app/
dest: /opt/app/
- name: Start application
ansible.builtin.service:
name: myapp
state: started
Collect Facts Once, Use Everywhere
- name: Get cluster version from any node
ansible.builtin.command: kubectl version --short
run_once: true
register: k8s_version
delegate_to: "{{ groups['k8s_masters'][0] }}"
- name: All hosts know the cluster version
ansible.builtin.debug:
msg: "K8s: {{ k8s_version.stdout }}"
Create Shared Resource Once
- name: Create shared NFS export
ansible.builtin.file:
path: /exports/shared
state: directory
mode: "0755"
run_once: true
delegate_to: "{{ groups['nfs_server'][0] }}"
- name: Mount the share on all hosts
ansible.posix.mount:
path: /mnt/shared
src: "{{ groups['nfs_server'][0] }}:/exports/shared"
fstype: nfs
state: mounted
Send Notification Once
- name: Notify deployment complete
community.general.slack:
token: "{{ slack_token }}"
channel: "#deployments"
msg: "v{{ app_version }} deployed to {{ ansible_play_hosts | length }} hosts"
run_once: true
delegate_to: localhost
run_once vs hosts: group[0]
Two approaches to run something once:
# Approach 1: run_once (task level)
- hosts: webservers
tasks:
- name: Migrate DB
command: /opt/migrate.sh
run_once: true
# Approach 2: target single host (play level)
- hosts: webservers[0]
tasks:
- name: Migrate DB
command: /opt/migrate.sh
| Feature | run_once: true | hosts: group[0] |
|---|---|---|
| Variable sharing | ✅ Result available on all hosts | ❌ Only on targeted host |
| Mixed with per-host tasks | ✅ Same play | ❌ Needs separate play |
| serial behavior | ⚠️ Once per batch | ✅ Truly once |
| strategy: free | ⚠️ Unpredictable | ✅ Safe |
Troubleshooting
Task Runs Multiple Times with serial
Expected behavior — run_once runs once per batch. Use a separate play targeting a single host if you need truly once.
"Variable is undefined" on Other Hosts
If the run_once task fails or is skipped due to a when condition, the registered variable won't be set on any host. Add a default:
- name: Use result with fallback
ansible.builtin.debug:
msg: "{{ my_result.stdout | default('not available') }}"
Related Articles
- Ansible delegate_to Explained
- Ansible serial — Rolling Updates
- Ansible Loops Guide
- Ansible Best Practices
- Ansible Check Mode (Dry Run)
Conclusion
run_once: true executes a task on one host while making the result available to all hosts. Use it for database migrations, notifications, shared resource creation, and any task that should happen exactly once. Key gotchas: it runs once per batch with serial, and behaves unpredictably with strategy: free. For truly single-execution tasks in rolling updates, target hosts: group[0] in a separate play instead.