## TL;DR

A macro calls itself (directly or through a cycle of macros) without a stopping condition, and Python's recursion limit kills the render. Find the cycle in the traceback, add a depth counter with a maximum, or rewrite the logic as a loop. Then recompile.

## Error

```text
"Failed to render model: maximum recursion depth exceeded" dbt macro
```

## Steps

1. Re-run with `--debug` and read the traceback: the repeating macro frame names the cycle. Expected: you see the same macro (or macro pair) calling itself.
2. Open the macro and find the recursive call. Expected: you see the call with no base case, or a base case that never triggers.
3. Add a depth parameter with a hard stop, for example `{% macro walk(node, depth=0) %}{% if depth > 50 %}{{ raise('too deep') }}{% endif %}...`. Expected: runaway recursion now fails fast with a clear message instead of a depth error.
4. Better, rewrite the recursion as an explicit loop over a work list when the logic allows it. Expected: no recursive macro calls remain.
5. Test with `dbt run-operation [MACRO NAME] --args '{...}'`, then `dbt compile`. Expected: the macro completes and the project compiles.

## When to use

- dbt fails with "maximum recursion depth exceeded" during rendering.
- You just added recursion to a macro (tree walks, nested struct flattening).

## When not to use

- The nesting is legitimately deep but finite (raise the limit instead of rewriting).
- The error happens in Python adapter code rather than your macro.

## Tool compatibility

- dbt Core 1.0 and later, all adapters. Jinja recursion limits come from Python.

## Variant phrasings

### RecursionError in macro during dbt compile

The raw Python form of the same failure.

### Macro hangs instead of erroring

A cycle that grows slowly can look like a hang; the depth guard turns it into a fast, clear failure.

## Why it happens

Jinja macros can call other macros, including themselves. Without a base case, or with a base case that never matches the real data, the call stack grows until Python refuses.

## Edge cases

- Mutual recursion (macro A calls B calls A) hides the cycle; the traceback shows the alternating frames.
- Data-driven recursion over deeply nested JSON can exceed the limit on legitimate data; raise the limit cautiously and add the guard anyway.
- `run-operation` reproduces macro recursion without compiling the whole project, which is much faster to iterate on.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_MyqI4ULWsY3hpvOXGfJ3Dg
