The short answer
Give the AI agent its own Postgres role and set four timeouts on it with ALTER ROLE: statement_timeout (kills long queries), lock_timeout (stops waiting on locks), idle_in_transaction_session_timeout (ends sessions that leave a transaction open) and, on PostgreSQL 17 or later, transaction_timeout (caps a whole transaction).112 Role settings apply at login, so every connection the agent opens starts with them.3
In our run on PostgreSQL 18.6, each timeout did its job. But all four are ordinary user settings, and the agent’s role turned them off with a single SET.4 Timeouts protect the database from accidents, not from a session that decides to ignore them.
Deedglass
A database change, checked and put back
-
1The agent’s change
Change order 42
UPDATE orders … WHERE id = 42
The agent: “1 row updated” -
2What the database really did
301 rows
A hidden trigger changed 300 more
Doesn’t match -
3Put back
301 rows restored
Signed off by a person
Matches
The four timeouts
Agents are good at writing queries and bad at knowing what a query costs on a large table. They also leave transactions open while they “think” between steps. Each timeout covers a different way that goes wrong.
| Setting | What Postgres does when it fires | What it protects |
|---|---|---|
statement_timeout |
Cancels the statement1 | CPU and I/O from a runaway query |
lock_timeout |
Cancels a statement still waiting for a lock1 | Your app’s queue behind a blocked lock |
idle_in_transaction_session_timeout |
Terminates the session1 | Locks held, and vacuum blocked, by an open transaction |
transaction_timeout (17+) |
Terminates the session1 | The total time any transaction can run |
A few details from the docs matter in practice:
- All four default to 0, which means off. A bare number is read as milliseconds.1
statement_timeoutruns from when the command arrives until the server finishes it. If several statements arrive in one simple-query message, each gets its own timer.1lock_timeoutcounts each lock attempt separately, and there’s no point setting it at or above a nonzerostatement_timeout, which would always fire first.1- An idle open transaction costs more than you’d think. The docs note it can hold locks and stops vacuum from removing dead rows, so tables bloat.1
transaction_timeoutarrived in PostgreSQL 17, released on 26 September 2024. If it’s shorter than or equal tostatement_timeoutoridle_in_transaction_session_timeout, the longer one is ignored. Prepared transactions are exempt.2
There’s a fifth, idle_session_timeout, for sessions idle outside a transaction. The docs warn to be careful with it behind connection poolers, which may not handle a closed connection well.1 Most agents connect through a pool, so we leave it off.
Set them on the agent’s role
The docs say not to set statement_timeout, lock_timeout or transaction_timeout in postgresql.conf, because they would hit every session, including your migrations and reports.1 Put them on the role the agent logs in as:
Code A database command in SQL. The text around it says what it does.
-- once, as an admin
ALTER ROLE agent SET statement_timeout = '5s';
ALTER ROLE agent SET lock_timeout = '2s';
ALTER ROLE agent SET idle_in_transaction_session_timeout = '30s';
ALTER ROLE agent SET transaction_timeout = '60s'; -- PostgreSQL 17+
The values become the session defaults whenever that role starts a new session. They are applied at login only, so SET ROLE agent from another session won’t pick them up.3 If the agent uses more than one database, ALTER ROLE agent IN DATABASE app SET ... overrides the role-wide value for that database.3
This only works if the agent has its own role. If it shares your application’s login, you can’t give it tighter limits without slowing your app. Our safe Postgres access guide covers creating that role, and default privileges covers keeping its grants narrow as tables are added.
What happened when we ran them
We started a throwaway PostgreSQL 18.6 cluster on a laptop, created a 1,000-row orders table, a login role agent with SELECT and UPDATE on it, and the four settings above. Then we connected as agent.4
At login, pg_settings showed each value with source user, meaning it came from the role:
Code What the screen or file shows, copied as is.
idle_in_transaction_session_timeout|30000|ms|user
lock_timeout|2000|ms|user
statement_timeout|5000|ms|user
transaction_timeout|60000|ms|user
Then we tried four ways an agent can stall a database:
| Test | Result |
|---|---|
SELECT pg_sleep(10) |
ERROR: canceling statement due to statement timeout |
UPDATE orders ... while an admin held LOCK TABLE orders |
ERROR: canceling statement due to lock timeout |
BEGIN, a query, then silence (with the idle limit lowered to 1 s) |
FATAL: terminating connection due to idle-in-transaction timeout |
BEGIN and two 2-second sleeps under a 3-second transaction limit |
FATAL: terminating connection due to transaction timeout |
Note the difference in the last two rows. A statement or lock timeout cancels one statement and leaves the session usable. The idle and transaction timeouts end the whole connection, and whatever the transaction did is rolled back.4 An agent framework that doesn’t expect a dropped connection may retry blindly, so check what yours does. Our post on agents that retry and write twice shows why that matters.
The limit: the role can turn them off
Every one of these settings has the user context in pg_settings. The docs say any user can change their own session value for such settings with SET.4 So we tried, as agent:
Code What the screen or file shows, copied as is.
SET statement_timeout = 0;
SELECT pg_sleep(6); -- ran to completion
ALTER ROLE agent SET statement_timeout = 0;
ALTER ROLE
Both worked. The second is worse: the docs confirm ordinary roles can set their own defaults3, so the agent removed its limit for every future session too. GRANT SET ON PARAMETER doesn’t help here; we ran REVOKE SET ON PARAMETER statement_timeout from PUBLIC and from agent, and the next SET still succeeded.4
That’s not a Postgres bug. These are comfort settings for a client, not a security boundary. For an agent, it means:
- Timeouts catch mistakes. A query the agent didn’t realise was expensive, a lock it didn’t expect, a transaction it forgot. That’s most of the real risk.
- They don’t stop a session that wants more time. A prompt-injected agent, or one told “the query keeps timing out, raise the limit”, can lift them. Watch your logs for
SET statement_timeoutandALTER ROLEfrom the agent’s role. - Real limits live outside the session. A gateway or proxy that sees each statement before it reaches Postgres can refuse
SETandALTER ROLEfrom the agent, and cap time itself. Grants (what the role can touch) are a true boundary; timeouts are not.
The same is true of default_transaction_read_only, which also has the user context.4 A role told to be read-only by default can SET itself out of it. Use GRANT and REVOKE for read-only, as our read-only access glossary entry explains.
Choosing values
There’s no right number for every database, but these starting points are easy to defend:
statement_timeout: a few seconds for an interactive agent. Long enough for an indexed query, short enough that a full-table scan on a big table fails fast. Raise it per task, not per role.lock_timeout: shorter thanstatement_timeout. Otherwise it never fires.1 One or two seconds keeps the agent from queuing behind (or in front of) your app.idle_in_transaction_session_timeout: tens of seconds. Agents pause between tool calls while the model thinks. Too short and normal multi-step work gets killed; too long and locks pile up.transaction_timeout: the longest task you’d accept, and longer than the other two, or they’re ignored.1
If the agent runs migrations, give migrations a separate role with its own limits. A migration on a large table may need minutes and a lock, which is exactly what you don’t want the everyday agent role to have. Our post on letting agents write migrations safely has the pattern.
A checklist
- Give the agent its own login role. Never share the application’s.
ALTER ROLE agent SETall four timeouts; check them withpg_settingsafter logging in as the agent.3- Keep
lock_timeoutbelowstatement_timeout, andtransaction_timeoutabove both.11 - Make sure the agent framework handles a cancelled statement and a dropped connection without retrying a write blindly.
- Alert on
SET statement_timeout,SET lock_timeoutandALTER ROLEsent by the agent’s role. - Treat grants, not timeouts, as the boundary. Put anything the agent must not be able to undo outside its session.
To see a gate refuse statements before they reach a real database, open the playground in your browser. To grade what your coding agents can reach on your own machine, including database URLs in their configs, run npx deedglass scan (the grade, explained).
Written with AI assistance and checked against our claims register.