Summary
I'm seeing a significant performance regression for templated variables after upgrade from 2.18 to 2.19. An example I have degraded from 0.28s to 10.98s
Issue Type
Bug Report
Component Name
core
Ansible Version
ansible [core 2.19.8]
python version = 3.14.4 (main, Apr 16 2026, 22:56:09) [Clang 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd7080 (/usr/local/bin/python3.14)
jinja version = 3.1.6
pyyaml version = 6.0.3 (with libyaml v0.2.5)
Configuration
# if using a version older than ansible-core 2.12 you should omit the '-t all'
$ ansible-config dump --only-changed -t all
ANSIBLE_PIPELINING(/usr/local/etc/ansible/ansible.cfg) = True
CALLBACKS_ENABLED(/usr/local/etc/ansible/ansible.cfg) = ['profile_tasks']
DEFAULT_BECOME_FLAGS(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E
DEFAULT_FORKS(/usr/local/etc/ansible/ansible.cfg) = 15
DEFAULT_HASH_BEHAVIOUR(/usr/local/etc/ansible/ansible.cfg) = replace
DEFAULT_LOAD_CALLBACK_PLUGINS(/usr/local/etc/ansible/ansible.cfg) = False
DEFAULT_LOG_PATH(/usr/local/etc/ansible/ansible.cfg) = /data/log/ansible/ansible.log
DEFAULT_POLL_INTERVAL(/usr/local/etc/ansible/ansible.cfg) = 15
DEFAULT_REMOTE_USER(/usr/local/etc/ansible/ansible.cfg) = ansible
DEFAULT_TIMEOUT(/usr/local/etc/ansible/ansible.cfg) = 30
EDITOR(env: EDITOR) = mcedit
HOST_KEY_CHECKING(/usr/local/etc/ansible/ansible.cfg) = True
INJECT_FACTS_AS_VARS(/usr/local/etc/ansible/ansible.cfg) = False
INTERPRETER_PYTHON(/usr/local/etc/ansible/ansible.cfg) = auto_silent
INVENTORY_ENABLED(/usr/local/etc/ansible/ansible.cfg) = ['ini', 'script']
PAGER(env: PAGER) = less
PERSISTENT_CONNECT_TIMEOUT(/usr/local/etc/ansible/ansible.cfg) = 30
TRANSFORM_INVALID_GROUP_CHARS(/usr/local/etc/ansible/ansible.cfg) = ignore
USE_PERSISTENT_CONNECTIONS(/usr/local/etc/ansible/ansible.cfg) = True
GALAXY_SERVERS:
BECOME:
======
runas:
_____
become_flags(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E
su:
__
become_flags(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E
sudo:
____
become_flags(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E
CALLBACK:
========
default:
_______
result_format(/usr/local/etc/ansible/ansible.cfg) = yaml
minimal:
_______
result_format(/usr/local/etc/ansible/ansible.cfg) = yaml
OS / Environment
FreeBSD 14.3 control host, Ubuntu 24.04 client
Steps to Reproduce
An example of a variable that combines data from a dynamic inventory (each host receives a dict with inventory data), any playbook-level host_vars/group_vars modifiers/options/amendments/corrections/overrides, actual addresses from gathered facts into a finalized source-of-truth interfaces data structure accessed from a ton of roles/playbooks.
---
heavy50__interfaces_combined: |-
{% set ns = namespace(out={}, interfaces_precombined={}, fact_v4=[], fact_v6=[]) %}
{# assemble interface defaults #}
{% set interface_defaults_combined = interface_defaults | combine(*(query('vars', *(query('varnames', '^interface_defaults_amend__.+') | sort))), recursive=True) %}
{# preassemble interfaces with tweaks #}
{% set ns.interfaces_precombined = interfaces|default({}) | combine(*(query('vars', *(query('varnames', '^interfaces_amend__.+') | sort))), recursive=True) %}
{# if dynamic_inventory is enabled in defaults, also add other ifaces from dynamic_inventory as empty shells for iteration #}
{% if interface_defaults_combined.dynamic_inventory|default(False)|bool %}
{% for item in dynamic_inventory.interfaces.keys()|default([]) if item not in ns.interfaces_precombined %}
{% set _ = ns.interfaces_precombined.update({item: {}}) %}
{% endfor %}
{% endif %}
{# iterate over precombined interfaces #}
{% for item, values in ns.interfaces_precombined|dictsort %}
{# decide if to load info from dynamic_inventory #}
{% set from_dynamic_inventory = values.dynamic_inventory|default(interface_defaults_combined.dynamic_inventory)|default(False)|bool and dynamic_inventory.interfaces[item] is defined %}
{% if from_dynamic_inventory %}
{# determine if to include ips of child ifaces #}
{% set from_dynamic_inventory_child_ips = values.dynamic_inventory_child_ips|default(interface_defaults_combined.dynamic_inventory_child_ips)|default(True)|bool %}
{# load info from dynamic_inventory, first pass: ips and ifnames #}
{% set values_from_dynamic_inventory =
{} | combine(
{'ip4': [], 'ip6': []},
dynamic_inventory.interfaces[item]
| dict2items
| selectattr('key', 'in', ['ifname', 'ip4', 'ip6'])
| selectattr('value')
| items2dict,
dynamic_inventory.children_inv
| map('extract', hostvars, ['dynamic_inventory', 'interfaces'])
| map('dict2items')
| flatten
| map(attribute='value')
| selectattr('parent', 'equalto', item)
| map('dict2items')
| map('selectattr', 'key', 'in', ['ip4', 'ip6'] )
| map('items2dict')
| list
if from_dynamic_inventory_child_ips else {},
list_merge='append_rp'
)
%}
{# second pass: additional and derivative values #}
{% set _ = values_from_dynamic_inventory.update(
{
"dns4":
dynamic_inventory_global.networks[
values_from_dynamic_inventory.ip4[0]|default('noip', True)
| ansible.utils.ipv4('network/prefix')
].dns|default(['noip'], True) | ansible.utils.ipv4('address'),
"dns6":
dynamic_inventory_global.networks[
values_from_dynamic_inventory.ip6[0]|default('noip', True)
| ansible.utils.ipv6('network/prefix')
].dns|default(['noip'], True) | ansible.utils.ipv6('address'),
"ip4_gateway":
dynamic_inventory_global.networks[
values_from_dynamic_inventory.ip4[0]|default('noip', True)
| ansible.utils.ipv4('network/prefix')
].gw|default('noip', True) | ansible.utils.ipv4('address'),
"ip6_gateway":
dynamic_inventory_global.networks[
values_from_dynamic_inventory.ip6[0]|default('noip', True)
| ansible.utils.ipv6('network/prefix')
].gw|default('noip', True) | ansible.utils.ipv6('address'),
"ip4_method":
('sync-auto' if ansible_facts.system == 'FreeBSD' else 'auto')
if dynamic_inventory.interfaces[item].dhcp|default(False)
else 'manual'
if values_from_dynamic_inventory.ip4|default([]) is truthy
else '',
"ip6_method":
('sync-auto' if ansible_facts.system == 'FreeBSD' else 'auto')
if dynamic_inventory.interfaces[item].dhcp6|default(False)
else 'manual'
if values_from_dynamic_inventory.ip6|default([]) is truthy
else '',
}
| dict2items
| selectattr('value')
| items2dict,
) %}
{% else %}
{# empty data if not loading info from dynamic_inventory #}
{% set values_from_dynamic_inventory = {} %}
{% endif %}
{# special treatment for ifname #}
{% set ifname = values.ifname|default(values_from_dynamic_inventory.ifname, True)|default(item, True) %}
{# insert dhcp addresses from ansible_facts #}
{% set ns.fact_v4 = [] %}{% set ns.fact_v6 = [] %}
{% if ifname is truthy %}
{% for addr_dict in [ansible_facts[ifname].ipv4|default({})]|flatten + ansible_facts[ifname].ipv4_secondaries|default([]) %}
{% set addr = '{}/{}'.format(addr_dict.address|default('noip'), addr_dict.netmask|default('noip')) | ansible.utils.ipv4('cidr') %}
{% set _ = ns.fact_v4.append(addr) if addr is truthy else False %}
{% endfor %}
{% endif %}
{% if ifname is truthy %}
{% for addr_dict in ansible_facts[ifname].ipv6|default([])|flatten %}
{% set addr = '{}/{}'.format(addr_dict.address|default('noip'), addr_dict.prefix|default('noip')) | ansible.utils.ipv6('cidr') %}
{% set _ = ns.fact_v6.append(addr) if addr is truthy else False %}
{% endfor %}
{% endif %}
{# finalize iface and prepend nameservers if any #}
{% set _ = ns.out.update(
{
item:
interface_defaults_combined
| combine(
values_from_dynamic_inventory,
values|default({}, True),
{"ifname": ifname, "ip4_fact": ns.fact_v4, "ip6_fact": ns.fact_v6},
recursive=True
)
| combine(
{
"dns4": nameservers_prepend|default(['noip']) | ansible.utils.ipv4('address'),
"dns6": nameservers_prepend|default(['noip']) | ansible.utils.ipv6('address'),
},
list_merge='prepend_rp'
)
}
) %}
{# end of iface iteration #}
{% endfor %}
{{ ns.out }}
Expected Results
Renders in a fraction of a second
works on:
ansible [core 2.18.7]
python version = 3.14.3 (main, Apr 2 2026, 22:54:57) [Clang 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd7080 (/usr/local/bin/python3.14)
jinja version = 3.1.6
libyaml = True
Actual Results
Code of Conduct
Summary
I'm seeing a significant performance regression for templated variables after upgrade from 2.18 to 2.19. An example I have degraded from 0.28s to 10.98s
Issue Type
Bug Report
Component Name
core
Ansible Version
Configuration
OS / Environment
FreeBSD 14.3 control host, Ubuntu 24.04 client
Steps to Reproduce
An example of a variable that combines data from a dynamic inventory (each host receives a dict with inventory data), any playbook-level host_vars/group_vars modifiers/options/amendments/corrections/overrides, actual addresses from gathered facts into a finalized source-of-truth interfaces data structure accessed from a ton of roles/playbooks.
Expected Results
Renders in a fraction of a second
works on:
Actual Results
Renders in 10 secondsCode of Conduct