kestra-io/kestra · error · IllegalVariableEvaluationException
Too many rendering attempts
Error message
Too many rendering attempts
What it means
Thrown by VariableRenderer when recursive rendering exceeds MAX_RENDERING_AMOUNT (100 iterations). The renderer re-evaluates the template output after each pass, stopping when the result stops changing. If every pass produces new Pebble syntax (the output keeps mutating), the loop never converges and this guard fires after 101 render cycles.
Source
Thrown at core/src/main/java/io/kestra/core/runners/VariableRenderer.java:172
return result;
}
private static String replaceRawTags(Matcher rawMatcher, Map<String, String> replacers) {
return rawMatcher.replaceAll(matchResult ->
{
var uuid = UUID.randomUUID().toString();
replacers.put(uuid, matchResult.group(1));
return uuid;
});
}
public Object renderRecursively(Object inline, Map<String, Object> variables, boolean stringify) throws IllegalVariableEvaluationException {
return this.renderRecursively(0, inline, variables, stringify);
}
private Object renderRecursively(int renderingCount, Object inline, Map<String, Object> variables, boolean stringify) throws IllegalVariableEvaluationException {
if (renderingCount > MAX_RENDERING_AMOUNT) {
throw new IllegalVariableEvaluationException("Too many rendering attempts");
}
Object result = this.renderOnce(inline, variables, stringify);
if (result == null || Objects.equals(result, inline)) {
return result;
}
return renderRecursively(++renderingCount, result, variables, stringify);
}
public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables) throws IllegalVariableEvaluationException {
return this.render(in, variables, this.variableConfiguration.getRecursiveRendering());
}
public Map<String, Object> render(Map<String, Object> in, Map<String, Object> variables, boolean recursive) throws IllegalVariableEvaluationException {
Map<String, Object> map = new LinkedHashMap<>();
for (Map.Entry<String, Object> r : in.entrySet()) {View on GitHub (pinned to 823fada927)
Solutions
- Identify the self-referential or infinitely-expanding variable by examining the flow's outputs at each render pass; trace which variable's value keeps producing new template syntax.
- Break the recursion cycle: ensure the rendered output of any variable does not itself contain unresolvable '{{ }}' or '{% %}' that maps back to the same variable chain.
- Wrap the dynamic content in '{% raw %}...{% endraw %}' blocks so the renderer does not re-evaluate it on subsequent passes.
- Flatten deeply chained variable lookups into a single non-recursive expression, or compute the value in a script task instead of in the template engine.
Example fix
# before (self-referential — never converges)
vars:
greeting: "Hello {{ vars.greeting }}"
# after (no recursion)
vars:
name: "World"
greeting: "Hello {{ vars.name }}" Defensive patterns
Strategy: try-catch
Validate before calling
// In Java, before calling render, scan the string for self-referential variable patterns
boolean isLikelySelfReferential(String template, String varName) {
return template != null && varName != null
&& template.contains("{{ vars." + varName)
&& template.contains(varName);
} Try / catch
try {
String rendered = variableRenderer.render(template, variables);
} catch (IllegalVariableEvaluationException e) {
if (e.getMessage().contains("Too many rendering attempts")) {
// log the template and variables for diagnosis, break recursion manually
log.warn("Recursive rendering loop detected in template: {}", template);
}
throw e;
} Prevention
- Avoid variable definitions where the rendered output contains the variable's own name or reference.
- Use {% raw %} blocks around content that should not be re-evaluated by the recursive renderer.
- Test new flow variables with a small data set first to catch expansion loops early.
- Set variableConfiguration.recursiveRendering to false if recursive rendering is not needed.
When it happens
Trigger: A flow variable whose rendered value itself contains '{{ }}' or '{% %}' that re-expands into more template syntax on the next pass, creating an infinite expansion chain. Common trigger: a variable like '{{ vars.selfreferential }}' where the resolved value embeds another reference to itself, or a subflow output that loops template variables back in. Also triggered when one variable resolves to a string containing another variable name that resolves to yet another, 100+ levels deep.
Common situations: Dynamic variable construction where outputs feed back as inputs (e.g., building JSON that itself contains template expressions). Subflow chaining with deeply nested outputs. Using a variable to generate the key of another variable recursively. Templating that produces '{{ }}' literally inside the rendered result.
Related errors
- Maximum render() nesting depth (%s) exceeded at line %s — ch
- The 'subflow' function exceeded the maximum nesting depth of
- The '%s' filter expects an integer as argument 'amount'.
- The '%s' filter expects a positive integer >=0 as argument '
- Unknown indent type '%s'.
AI-assisted analysis of kestra-io/kestra@823fada927 (2026-08-14).
Data as JSON: /api/errors/516062c4574834d0.
Report an issue: GitHub.