Debug a Database Without Connecting to It

Necco Ceresani
Necco Ceresani
·12 min read

Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.

Last updated: October 2026

Quick answer. Yes, for a large class of problems. An agent can read every statement your application sends Postgres or MySQL off the socket, with the values the driver bound, without a role, a password or a connection to the database. That covers N+1 patterns, query storms, unexpected statements from a service that should not be issuing them, and which process sent any of it. What it cannot do is explain a query plan, because whether an index was used exists only inside the database and EXPLAIN is the only thing that knows. The useful split is that what your application sent is observable from your own host, while what the database did about it is not.

I build eBPF tooling at yeet, so I spend a lot of time on the question of what can be observed without being granted anything. I am not going to tell you whether your security team should approve an agent touching production, and I have no view on your schema. The claim I want to make precise is narrower and more checkable: a read-only guarantee that comes from a tool having no write path is a different kind of assurance than one that comes from a GRANT, and the difference matters most in exactly the situation where people are most nervous.

Why does giving an agent a Postgres connection make people nervous?

Because a connection is a capability, and capabilities granted to an agent are harder to reason about than the same capabilities granted to an ordinary program. A conventional script does a fixed thing with its connection, and you can read the script. An agent decides what to do with its connection at runtime, based on a model's judgment about a prompt, and the set of statements it might issue is not enumerable in advance.

That is the structural worry, and the usual mitigations address it only partly. A read-only role bounds writes but not reads, so every row the role can see is a row the agent can retrieve and put into a context window that leaves your machine. Statement timeouts bound duration, not disclosure. A separate replica bounds blast radius to a copy of the same data. Each is reasonable and none of them makes the underlying question go away, because the thing you granted was the ability to ask the database for data.

There is a second worry that is less discussed and more mundane: the connection has to be configured. Someone creates a role, puts a password somewhere the agent can read it, and that credential now exists on disk, in an environment variable, or in an MCP server config, where it outlives the debugging session that justified it.

What can an agent learn from SQL traffic without a connection?

More than most people expect, because the statements themselves are the evidence for a surprising share of real problems. The reflex is to treat the database as the only authority on queries, when half of what you want to know is a fact about your own application that never required the database's cooperation to observe.

Every query your application sends leaves the process as bytes on a socket before the database ever sees it. A kernel probe on the socket write path reads those bytes and parses the wire protocol, which means the agent gets the statement text, the bound parameter values, the timing, and the identity of the sending process. sqlsnoop is the yeet script that does this for Postgres and MySQL.

The problems that become visible from traffic alone are the ones where the pattern, rather than any single query, is the bug:

  1. N+1 query patterns. An ORM fetches a list, then issues one query per row. Every individual query is fast and correctly indexed, so a slow query log stays empty and an APM shows a slow endpoint with nothing to blame. Counting statements by shape over a window is what makes it visible, and the count is the finding rather than the latency.
  2. Query storms and retry loops. A statement issued thousands of times in a burst, often by a retry path nobody remembered was there.
  3. Statements from a service that should not be sending them. The probe fires inside the sending process, so every statement arrives attached to a PID and a command name.
  4. The real parameter values. tcp_sendmsg sees the bytes the driver wrote, so customer_id = 4471 is an observation rather than a reconstruction from a prepared-statement template.

Point three deserves emphasis because it is the thing a database-side tool structurally cannot do. pg_stat_statements aggregates by statement text and does not record the client. The usual workaround, reading application_name from pg_stat_activity, stops identifying the real sender as soon as a connection pooler shares connections between services. Behind PgBouncer the sender is not recoverable from inside the database, and it is free from the socket.

Is reading SQL off the socket with eBPF actually read-only?

Yes, and the reason is worth separating from the usual version of this claim, because "read-only" is doing different work in the two cases. One is a rule the database agrees to apply, and the other is the absence of any mechanism capable of writing. Those sound similar in a policy document and behave differently when something goes wrong.

A read-only database role is a policy decision enforced by the database. It is reliable, and it is also a configuration that someone can change, that has a privilege escalation surface, and that depends on the role being set up correctly in the first place.

A socket reader has no write path at all. There is no connection, no session, and no statement it is capable of issuing, so there is nothing to misconfigure in a way that grants writes. The tool's own documentation states the limit directly: it "is observability, not enforcement," and it "reads bytes and never modifies, delays, or blocks a statement." The matching database-alerts prompt carries the same constraint into the agent's instructions, telling it to watch only and "never change my databases, their configuration or their users."

That is a property of the mechanism rather than a promise about behavior, which is the strongest form this kind of assurance takes. It does not depend on the agent being well-behaved, on the prompt holding, or on a role being configured correctly.

Does capturing SQL parameter values still expose customer data?

Yes, and anyone presenting this approach as a privacy win without saying so is overselling it. The credential question and the data-exposure question are separate, and solving the first does not touch the second. Teams that conflate them end up with a setup that is easier to approve than it is to justify.

Query traffic contains bound parameter values, and those values are frequently personal data. A WHERE email = ... carries an email address. Capturing real values is exactly what makes the tool useful for debugging, because a reconstructed template tells you the shape of the query and not which row was slow, and it is also what makes the captured traffic sensitive.

The accurate description is that the exposure has moved rather than disappeared. The agent no longer holds a credential capable of reading every row in the database, which is a real reduction, and it does see the values flowing through the statements it watched during the window it was running. For most debugging sessions that is a good trade. For a database where any row is regulated, treat the capture the way you would treat an application log that prints parameters, because that is what it is.

Two details bound the exposure in practice. The capture is held in memory, 2,000 statements on one host with no retention and no aggregation across machines, so quitting ends it. And statements are read into a fixed window, 256 bytes for the statement and 192 for the parameter block, so a long statement shows its opening rather than its full contents.

Which Postgres problems still require a database connection?

Anything whose answer lives in the database's own state, and the line between the two categories is clean rather than fuzzy. Once you have sorted a question onto the correct side of it, which tool to reach for stops being a judgment call.

QuestionAnswerable from trafficNeeds a connection
What statements did my app send, how often, from which processYes
Which parameter values were boundYes
Is this query using an index, what is the planEXPLAIN ANALYZE
How large is this table, how bloated is this indexCatalog queries
What is holding this lock right nowpg_locks
Which statements are expensive across all clients over weekspg_stat_statements

Read the right-hand column and a pattern emerges: it is all questions about the database's internal state and history, which the database is the only witness to. The left column is questions about your application's behavior, which your application is the witness to.

A sensible arrangement uses both and keeps the grants separate. The socket reader finds what is happening, with no credential involved. A read-only role, used deliberately, explains why the database is slow about it. Finding the N+1 does not require a credential, and deciding whether the fix is an index does.

What stops a socket reader from seeing my SQL queries?

Four limits apply, and the first rules the approach out entirely for some stacks, so it is worth checking against your own runtimes before planning around the tool rather than after. None of them is a configuration problem you can work around with a flag.

Go and Java clients over TLS are invisible. The TLS probes attach to SSL_read and SSL_write in OpenSSL. Go's crypto/tls is pure Go and Java's JSSE lives inside the JVM, so neither exposes a C symbol to hook. Plaintext connections from both are read normally. This limit is unavoidable rather than unimplemented.

Unix socket clients are invisible. The probes hook tcp_sendmsg, so a local client going through /var/run/postgresql/.s.PGSQL.5432 never appears. Connect over TCP to see it.

Latency under pipelining is approximate. Neither wire protocol carries a request id, so a reply is paired with the oldest outstanding send on its socket. That is correct for the ordered request and response cycle every normal SQL client uses, and a genuinely concurrent multiplexer over one connection would defeat it. The tool labels this socket-paired rather than presenting it as exact.

Processes that started before the probes attached. A uprobe only fires for processes that start afterwards, so a client already running when you attach stays invisible on the TLS path until it reconnects. Start the capture before the workload.

How do I capture SQL queries on a managed database like RDS?

The same way you run it anywhere, which is the main practical advantage of attaching at the socket. The probes live on your application host rather than on the database, so a managed instance you have no superuser access to and cannot restart stops being an obstacle to seeing your own queries.

curl -fsSL https://yeet.cx | sh    # install yeet, once
yeet run gh:yeet-src/sqlsnoop     # watch every Postgres and MySQL client on the host

Nothing is installed into the application, no driver is wrapped, and no redeploy is involved. Compare that with the database-side alternatives on a managed instance: log_statement = 'all' needs a configuration change and generates log volume proportional to traffic, and pg_stat_statements is a shared library requiring a restart, which you may not be permitted to perform on RDS or Cloud SQL at all.

For encrypted connections the TLS probes need a target binary, passed after -- so the flag reaches the script rather than yeet itself:

yeet run gh:yeet-src/sqlsnoop -- --tls-binary libssl.so
yeet run gh:yeet-src/sqlsnoop -- --tls-binary "$(command -v node)"

Ask what your application sent before asking Postgres

A large share of database debugging is really application debugging, and application behavior is observable from the host the application runs on. Reading statements off the socket answers those questions with no credential, no connection and no reconfiguration, which also makes it the approach that survives a managed instance and a connection pooler. Keep a database connection for the questions only the database can answer, and grant it when one of those questions actually comes up.

Frequently asked questions

Can an AI agent debug a database without a password?

Yes, for questions about what the application sent. A socket-level reader parses the Postgres and MySQL wire protocols from the sending process, so no role, password or connection is involved.

Can an agent find an N+1 query without connecting to the database?

Yes. N+1 is a count of statements by shape over a window, which is fully visible in traffic leaving the application. The database connection is not what makes the pattern detectable.

Is watching SQL traffic safer than giving an agent a read-only role?

It removes the credential and the ability to query arbitrary rows, which is a real reduction. It does not remove exposure to the parameter values in the statements it watched, so it moves the risk rather than eliminating it.

Can a socket reader modify my data?

No. It has no connection and issues no statements, so there is no write path to misconfigure. That follows from the mechanism rather than from a permission setting.

Does this work on RDS, Cloud SQL or a managed database?

Yes, because it attaches to your application host rather than the database instance. That is the main advantage over pg_stat_statements, which needs a restart you may not be able to perform.

Can an agent run EXPLAIN without a database connection?

No. A query plan exists only inside the database, and EXPLAIN requires a connection. Whether an index was used is invisible from the wire.

Does reading query traffic add load to my database?

No. The probes run on the application host and read bytes already being written, so any cost lands there rather than on the database server.

Will this capture queries sent over TLS?

Over OpenSSL, yes, with a --tls-binary target. Go's crypto/tls and Java's JSSE do not expose a hookable C symbol, so TLS traffic from those runtimes cannot be read.

Do I need to change application code to capture SQL statements?

No. The probes attach from outside the process, so no instrumentation library, driver wrapper or redeploy is required.

How long is captured query traffic kept?

It is held in memory, 2,000 statements for one host, with no retention or cross-machine aggregation. Quitting the tool ends the capture.

Sources

Related resources