Set a statement timeout on every role. That is the one control that turns this from an outage into a non-event.
The problem. A single runaway query - or a deliberately crafted one - can consume 100% CPU for minutes, blocking every other query. Without a timeout your database simply stays unresponsive until you notice and kill it by hand. That is an easy denial-of-service vector: it costs the attacker one request.
The fix is one line per role: ALTER ROLE myuser SET statement_timeout = '15s';. About 15 seconds is a sensible starting point for a read-only analytics API, where most legitimate queries finish in under five - pick your own number from your real query profile. Set it at the application level too (an environment variable your pool applies), so it covers every connection rather than only the roles you remembered.
Then add indexes - and treat them as a security control. This is the part people file under "performance" and skip. Without indexes, every query scans the entire table row by row. On a table with even ten thousand rows, that is slow enough that a few concurrent requests can tie up all your database connections. Index the columns you filter, sort, group and join by, and a full scan becomes a near-instant lookup - so even a malicious query resolves quickly instead of parking on your CPU.
Third: size the connection pool. Keep a few connections pre-warmed so requests are not paying cold-start latency, cap the maximum so the database is not overwhelmed, and set an acquire timeout so requests fail fast instead of hanging forever when every connection is busy.
Note this is the Postgres answer. DuckDB behaves completely differently - it runs inside your process, and its cancellation controls do not reliably stop a query at all. Full items: https://www.tigzig.com/security/database.
← All Agents FAQ