Prisma will tell you how long every query took. The problem is that it will tell you about every query, and on a busy route that is thousands of lines an hour in which the one you need is invisible.
The fix is small: stop printing queries, start listening for them, and only write the ones past a threshold.
stdout versus event
Prisma's log option takes either a level name or an object with emit:
// prints every query, unfiltered
new PrismaClient({ log: ["query"] });
// emits an event you can filter
new PrismaClient({ log: [{ emit: "event", level: "query" }] });With emit: "stdout" Prisma formats and prints the line itself and you have no say in it. With emit: "event" nothing is printed — you get a handler, with the SQL, the parameters and a duration in milliseconds, and you decide what is worth recording.
That distinction is the whole technique.
A threshold and a structured line
const prisma = new PrismaClient({
log:
process.env.NODE_ENV === "production"
? [
{ emit: "event", level: "query" },
{ emit: "stdout", level: "error" },
]
: [{ emit: "event", level: "query" }],
});
prisma.$on("query", (e) => {
if (e.duration >= 100) {
console.warn(
JSON.stringify({
level: "warn",
category: "database",
message: "Slow query detected",
query: e.query,
durationMs: e.duration,
timestamp: new Date().toISOString(),
})
);
}
});Errors still go straight to stdout — you want those unconditionally. Queries go through the handler and only survive if they are slow.
JSON rather than a formatted string because the consumer is a log aggregator or grep, not a human reading a terminal. One object per line stays parseable no matter what ends up inside the SQL.
Picking the threshold
100ms is a reasonable opening position for a web request: slow enough that normal indexed lookups never appear, fast enough to catch a missing index before users complain.
It is a starting point, not a rule. Two adjustments worth making once you have data:
- Too quiet? Lower it. If a week passes with no output, the threshold is above your worst query and you are monitoring nothing.
- Too noisy from one known offender? Resist raising the threshold globally — that hides everything else to silence one query. Fix it, or exclude it specifically.
Background jobs deserve a different number entirely. A nightly report taking four seconds is fine; the same four seconds inside a page render is not. If they share a client, tag the context rather than tuning one threshold to serve both.
The parameters question
Prisma's query event also carries e.params. It is genuinely useful — the same statement is fast for one input and slow for another, and without parameters you cannot tell which.
It is also the values themselves: emails, tokens, names, whatever the query filtered on. Logging them puts user data in a log stream that is often retained longer, and read more widely, than the database.
Decide deliberately. Omit them in production and keep them in development, or log them only for queries that touch no personal data. What you should not do is include them by reflex because the field was there.
What this will not catch
A duration threshold measures database time. It is silent about a query that is fast in Postgres and expensive everywhere after it.
The clearest example is over-fetching. findMany with no select returns every column, including large text bodies your page never renders. Postgres answers quickly; the cost is transfer and deserialisation, and the query sails under a 100ms threshold while the page feels slow.
So read the SQL these logs give you, not just the number beside it. If the SELECT lists columns the route does not use, you have found a problem the threshold was never going to report.
The same goes for N+1: fifty queries at 4ms each never trip a 100ms threshold, and together they are slower than the single join that should have replaced them. Watch the count per request as well as the duration of each.
Worth doing early
This is fifteen lines and no dependency, and it is far easier to add before you need it. The alternative is adding it during an incident, on a system already under load, and discovering what your slow queries were only after they became everyone's problem.