DEV Community

Vivek Kumar
Vivek Kumar

Posted on

Read-Only Isn't Enough: Query Guardrails That Keep AI From Taking Down Your Database

You did the responsible thing. Before pointing Claude, Cursor, or an in-app assistant at your database, you created a read-only role. INSERT, UPDATE, DELETE, DROP — all rejected. The AI can look but not touch. Safe, right?

Not quite. Read-only protects your data from being modified. It does nothing to protect your database server from being overwhelmed. And the way large language models write SQL — confidently, sometimes with a missing join condition or a forgotten WHERE — makes them very good at producing queries that are perfectly valid, perfectly read-only, and perfectly capable of pinning your CPU to 100% at 2pm on a Tuesday.

This is the guardrail layer most teams skip. It's not about permissions; it's about resource limits. Below are the guardrails that matter, why each one exists, and how to actually set them up. They're vendor-neutral and apply whether you're wiring up a raw connection, an MCP server, or an internal reporting tool.

The failure mode read-only doesn't cover

Imagine an AI assistant is asked, "How many orders did each customer place last year?" It knows there's an orders table and a customers table, so it writes:

-- The AI forgot the join condition
SELECT c.name, COUNT(*)
FROM customers c, orders o
GROUP BY c.name;
Enter fullscreen mode Exit fullscreen mode

That comma join with no ON clause is a cartesian product. If you have 50,000 customers and 2 million orders, the database tries to build a 100-billion-row intermediate result before it ever gets to counting. It's a legal SELECT. Your read-only role happily allows it. And it will chew through memory and CPU until something falls over.

The same thing happens with an innocent-looking SELECT * FROM events against a table with 500 million rows, or an unindexed ORDER BY on a huge dataset. None of these are attacks. They're just the ordinary output of a model that doesn't know how big your tables are. So the job of the guardrail layer is to make sure that even a badly-shaped query fails cheaply instead of expensively.

Guardrail 1: Cap every result set

The first and simplest defense: never let a query return an unbounded number of rows. Even if the AI writes SELECT * FROM events, wrap what it sends in an outer limit before executing:

-- What the AI generated
SELECT * FROM events WHERE user_id = 42;

-- What you actually run
SELECT * FROM (
  SELECT * FROM events WHERE user_id = 42
) AS ai_query
LIMIT 1000;
Enter fullscreen mode Exit fullscreen mode

A sensible default is somewhere between 500 and 1,000 rows. The AI almost never needs more than that to answer a question or summarize a trend, and the cap protects both your database (less data to materialize and sort) and the process consuming the results from an out-of-memory surprise. Make the limit configurable, but always have one on by default. "No limit" should never be the default state for a query you didn't write yourself.

Guardrail 2: Kill runaway queries with a statement timeout

A row limit caps what comes back, but a cartesian join can burn resources long before it produces a single row. That's what a statement timeout is for: it tells the database to abort any query that runs longer than a set duration.

In PostgreSQL you can set this per role, so it applies automatically to every session the AI opens:

-- Any query from ai_readonly that runs longer
-- than 30 seconds gets terminated automatically
ALTER ROLE ai_readonly SET statement_timeout = '30s';
Enter fullscreen mode Exit fullscreen mode

MySQL has an equivalent via max_execution_time (as a query hint or system variable). The exact number depends on your workload — 30 seconds is a common starting point for interactive/analytical use — but the principle is universal: a query the AI wrote should never be able to run indefinitely. When it hits the ceiling, the database cancels it and returns a clean error the AI can react to, instead of silently degrading everything else on the box.

Crunchy Data puts it well: a statement timeout ensures no connecting client "will have queries running longer than that." It's one line of config for an enormous reduction in blast radius.

Guardrail 3: Point the AI at a read replica, not production

The strongest structural guardrail isn't a setting — it's which server the AI talks to. If the assistant only needs to read and analyze, it never has any business touching your primary database. Point it at a read replica instead.

This buys you two things at once. First, exploratory and reporting queries don't compete with real user traffic for CPU, memory, or locks on your hot tables — a heavy analytical scan on the replica leaves checkout and login untouched on the primary. Second, a replica is read-only by the laws of physics: a PostgreSQL standby will reject writes no matter what, so even if you fumbled the read-only role config, there's a hard floor under you. It's defense in depth.

Setup Protects data from writes Protects production from load Operational cost
Full-access role on primary No No Low
Read-only role on primary Yes No Low
Read-only role + timeout + row cap on primary Yes Partially Low
Read replica with the same guardrails Yes Yes Higher

A replica does add operational overhead — a second server, replication lag to monitor, failover to think about — so it's not always worth it. For a small database or a tiny team, a read-only role with a timeout and a row cap on the primary gets you most of the safety. But once your database matters and your AI usage is more than occasional, the replica is the cleanest boundary you can draw.

Guardrail 4: Constrain the connection itself

Timeouts and row limits handle a single bad query. A connection limit handles many bad queries. If an AI agent (or a customer-facing feature powered by one) can open unlimited concurrent sessions, a burst of activity can exhaust your connection pool and lock out your real application.

-- Cap how many simultaneous connections
-- the AI's role can hold open
ALTER ROLE ai_readonly CONNECTION LIMIT 5;
Enter fullscreen mode Exit fullscreen mode

Combine that with the statement timeout from earlier and, as one Postgres engineer put it, a "statement timeout and connection limit on a read-only role gives you 90% of the safety with 10% of the complexity." It's a great return on two lines of SQL.

Guardrail 5: Inspect the query shape (advanced)

For higher-stakes setups, you can validate the structure of a generated query before running it. A reporting query has a predictable shape: SELECT ... FROM ... WHERE .... If one suddenly references system catalogs, stacks multiple UNIONs, or joins a dozen tables, you can reject it at the application layer before it reaches the database. This is easy to over-engineer, so treat it as a layer you add when the earlier four aren't enough — the row cap, timeout, and replica cover the common cases with far less effort.

Where MCP fits

A lot of teams reach for the Model Context Protocol to connect AI clients to their databases, and a well-built MCP server is a natural home for these guardrails: the AI client sends a request, and the server — not the model — decides what actually runs. That's exactly the right place to enforce SELECT-only access, an outer row limit, and a statement timeout, so the same rules apply no matter which AI tool is on the other end.

You can build this yourself, or use a managed MCP server that ships with the guardrails in place — Draxlr's, for example, is read-only (SELECT only) and sits between the AI and your database — so you inherit the safe defaults instead of hand-rolling them. Either way the point is the same: enforcement lives in the broker, where the model can't talk it out of them.

Common mistakes and gotchas

Assuming read-only means safe. It means safe from writes. Resource exhaustion is a separate problem with separate fixes.

Setting the timeout too high "just in case." A 10-minute timeout on an interactive assistant defeats the purpose. If a query genuinely needs longer, run it deliberately, not through the AI's default path.

Forgetting the timeout on the replica. Replicas need their own statement_timeout — the setting doesn't automatically follow from the primary. A runaway query on an unbounded replica is still a runaway query.

Relying on the AI to add its own LIMIT. It often will. It sometimes won't. Guardrails you enforce are the ones you can count on; guardrails you hope the model remembers are not guardrails.

No connection ceiling. Everyone tunes the single-query case and forgets that concurrency can take you down just as easily.

Key takeaways

Read-only access is the floor, not the ceiling, of safe AI database access. The queries that hurt you won't be malicious writes — they'll be well-meaning, valid SELECTs that scan too much. Cap every result set with an outer LIMIT, set a statement_timeout so runaway queries die cheaply, put a CONNECTION LIMIT on the role, and — when it's worth the cost — point the AI at a read replica so its worst query can't touch production. Enforce these in a broker or connection layer, not in the prompt, so they hold regardless of what the model writes.

Your turn

How are you protecting your database from AI-generated queries — read replica, tight timeouts, query inspection, or something else? Have you had an assistant produce a query that surprised you? Drop it in the comments; the war stories are usually the most useful part.


Sources: Crunchy Data — Control Runaway Postgres Queries With Statement Timeout, Rietta — Protect Production SQL Databases from AI/LLM Agentic SQL Query Risks, Model Context Protocol.

Top comments (0)