Now-Next

SaaS architecture

stack depth limit exceeded in PostgreSQL: four causes

What raised 'stack depth limit exceeded' in PostgreSQL 17, measured: triggers, recursion, a policy reading its table via a function, long expressions.

Chris van Eijk, Now-Next · · 7 min read

ERROR: stack depth limit exceeded comes with a hint that sends you the wrong way: Increase the configuration parameter "max_stack_depth" (currently 2048kB), after ensuring the platform's stack depth limit is adequate. In the loops we reproduced — a trigger, a recursive function, a policy — raising the limit only moved the error further down. A long generated expression is different: it is deep, not endless, and a higher limit does let it through. The SQLSTATE is 54001.

We reproduced it on PostgreSQL 17.11 with the default max_stack_depth of 2 MB, in four ways: a trigger that updates its own table, a function that calls itself, a row-level-security policy that reaches its own table through a function, and one very long expression. For each we counted how deep it got before the error and checked the fix.

What does “stack depth limit exceeded” mean in PostgreSQL?

A PostgreSQL backend checks how much of its process stack it has used at the points where it can go one level deeper — a function call, a trigger, the next level of an expression — and stops with this error once that passes max_stack_depth. The documentation describes the setting as “the maximum safe depth of the server’s execution stack”: it keeps a runaway recursion from crashing the backend process. The statement fails like any other error. Each level uses a different amount of stack, so how deep it gets depends on what is nesting: at 2 MB a self-updating trigger stopped at depth 788, a recursive SQL function after 7,467 calls.

What the hint buys: the same three cases at max_stack_depth 2 MB and 7 MB
Case and settingCalls or terms before the error
PL/pgSQL function calling itself — 2 MB915
PL/pgSQL function calling itself — 7 MB3,206
SQL function calling itself — 2 MB7,467
SQL function calling itself — 7 MB26,191
1+1+…+1, terms that still ran — 2 MB4,092
1+1+…+1, terms that still ran — 7 MB14,332

Bar length is logarithmic, from 100 to 26,191: every tenfold increase adds the same length. Each case got about 3.5 times further at 7 MB. The two functions still ended in the error; the expression of 4,093 terms that failed at 2 MB ran at 7 MB.

A trigger that updates its own table

A common way to get there in an application. An AFTER UPDATE row trigger that sets a column with an UPDATE on the same row fires itself again:

create function touch() returns trigger language plpgsql as $$
begin
  update t2 set updated_at = now() where id = new.id;
  return null;
end $$;
create trigger touch after update on t2 for each row execute function touch();

update t2 set n = n + 1 where id = 1;
-- ERROR:  stack depth limit exceeded

With a perform setval(…, pg_trigger_depth()) added as the first line, the last depth recorded before the error was 788. Three fixes, each measured on the same table:

FixResult
A BEFORE UPDATE trigger that sets new.updated_at := now() and returns new — no second UPDATEworked, trigger depth 1
WHEN (old.n is distinct from new.n) on the trigger, so the inner update, which leaves n alone, does not fire itworked, trigger depth 1
if pg_trigger_depth() > 1 then return null; end if; at the top of the functionworked: the inner update ran once and stopped there

The first is the one we would use: a trigger that changes the row being written does not need to write it again. Both the guard and WHEN still run the second UPDATE once; only the BEFORE trigger writes the row a single time. The guard would also skip the trigger when the update came from another trigger — we did not measure that.

A function that calls itself

A recursive function without a base case, or with one it never reaches. A PL/pgSQL function got 915 levels deep, an SQL function 7,467 calls. With max_stack_depth at 7 MB they got to 3,206 and 26,191, and failed the same way. A base case fixed both; for the SQL function we checked it separately.

A policy that reaches its own table through a function

A row-level-security policy whose condition reads the table it protects is caught before the query runs, with infinite recursion detected in policy for relation. Put the same subquery in a function and PostgreSQL cannot see the loop in advance: the policy calls the function, the function’s query triggers the policy again, and so on until the stack runs out. As an ordinary application role, with FORCE ROW LEVEL SECURITY on:

The policy’s conditionErrorLevels
inline subquery on the same tableinfinite recursion detected in policy for relation "docs"caught before running
SQL function reading the same tablestack depth limit exceeded1,816
PL/pgSQL function reading the same tablestack depth limit exceeded788
table A’s policy calls a function on B, B’s policy a function on Astack depth limit exceeded788, both functions together
inline subqueries, table A’s policy on B and B’s on Ainfinite recursion detected in policy for relation "a"caught before running
SECURITY DEFINER function owned by the table owner, FORCE on, policy also TO the ownerstack depth limit exceeded685

SECURITY DEFINER alone did not break the loop: the function runs as the table owner, and with FORCE the owner is subject to the policy too. What broke it was a function owner to whom the policy does not apply — with FORCE off, a role with BYPASSRLS, or a superuser — and then the query returned with the function called once. One variant failed without an error: with the policy written TO app only, the definer function owned by the table owner returned nothing, because no policy applied to the owner and FORCE made that a refusal of every row. The query counted 0 instead of 10.

A function owned by a BYPASSRLS role reads every row of the table, so the function’s own query has to do the filtering. The six ways out of a self-referencing policy, with their timings, are in infinite recursion detected in policy for relation; who else gets past a policy is in who bypasses row-level security.

One very long expression

Generated SQL can hit the limit without any loop. A chain of 1+1+…+1 worked with 4,092 terms and failed with 4,093; a chain of || and nested abs(abs(…)) calls failed at about the same length. A chain of 4,093 views, each selecting x + 0 from the one before, failed the same way when queried — the views are merged into one expression of 4,093 additions; querying the 4,092nd worked but took 16 seconds. Creating them, up to 5,001, did not fail.

Other shapes fail differently or not at all:

ShapeResult
x = 1 or x = 2 or …, flatno error up to 300,000 terms (about 7 seconds)
x <> 1 and x <> 2 and …, flatno error up to 300,000 terms
x in (1, 2, …)no error up to 1,000,000 values (0.77 seconds)
x = 1 or (x = 2 or (…)), nestedmemory exhausted at or near "3331" from 3,331 terms
((((1)))), nested parenthesesmemory exhausted at or near "(" from 9,994 levels

memory exhausted (SQLSTATE 42601) comes from the SQL parser’s own stack, not from max_stack_depth, and raising the setting does not change it: at 7 MB the same three shapes failed with the same message. If a report builder produces either error for a long filter, the shapes that held were flat OR and AND chains and IN (…) lists; a chain of + or || failed even without parentheses. Our IN (…) list of a million values — literals, not bind parameters — ran in 0.77 seconds.

Should you raise max_stack_depth?

Only when the nesting is real and bounded, and even then it is a superuser’s decision. As an ordinary role, SET max_stack_depth — even to lower it — failed with permission denied to set parameter "max_stack_depth". A superuser can ALTER ROLE app SET max_stack_depth = '4MB', which applied from the role’s next login, while the role still could not SET it within a session; since PostgreSQL 15, GRANT SET ON PARAMETER max_stack_depth TO app lets the role set it itself.

The ceiling is the operating system’s stack limit for the server process. In our container ulimit -s was 8,192 kB, and PostgreSQL refused anything above 7,680 kB: "max_stack_depth" must not exceed 7680kB, with the hint to raise the platform’s limit first. From 2 MB to 7 MB the depth grew about 3.5 times in the three cases we tried — the PL/pgSQL and SQL functions and the + chain — and the two loops still ended in the error.

What we did not measure

Other stack sizes than 2 MB and 7 MB, a raised ulimit -s, insert, delete and statement-level triggers, the trigger case at 7 MB, and a chain of views that does not build up an expression. We could not explain from these runs why the error context of the recursive SQL function lists about half as many calls as our counter.

Sources: PostgreSQL 17 documentation on max_stack_depth, read on 16 October 2026. All measurements are our own, on PostgreSQL 17.11 in a container, on 16 October 2026.

Let's talk

What are we building?

One conversation is enough to know whether we fit. Tell us what you have in mind — we will tell you how fast it can happen, and whether we are the right people for it.

Start the conversation