DEV Community

Sharon Alhazov
Sharon Alhazov

Posted on

My Node.js app was making 47 database queries per request. I had no idea.

I'd been shipping this endpoint for 6 months. It passed code review. It passed load tests. Here's what it was actually doing.

The output

I added 2 lines to a realistic Express + PostgreSQL demo app and ran it for a few minutes under normal load. This is what printed to the console:

[QUERY] SELECT * FROM quotes WHERE author_id = $1  47 executions in 1.8s
 n-plus-one: quotes.service.ts:34 fired 47 times in one request.
   Fix: batch with WHERE author_id = ANY($1) or use a dataloader.
   traceId: 3a9f2b  correlated with GET /quotes/by-author (durationMs: 2140ms)

[QUERY] SELECT * FROM authors  1 execution in 3ms
 select-star: Returning all 23 columns. Hot path only uses: id, name, bio.
   Consider: SELECT id, name, bio FROM authors

[MEMORY] Heap used: 187MB  203MB over 60s
 heap-growth: Consistent upward trend  no GC plateau detected.
   Check for accumulating closures or unbounded caches.

[EVENT LOOP] lag: 48ms | pollDelay: 61ms
 Elevated above baseline (< 10ms expected). Check for synchronous work in request handlers.
Enter fullscreen mode Exit fullscreen mode

Argus demo

Let me explain what each one means.


What it found

1. The N+1 query

GET /quotes/by-author fetches a list of quotes. For each quote, it then fetches the author with a separate query.

With 47 quotes in the response, that's 47 individual SELECT * FROM quotes WHERE author_id = $1 calls — one per row. The endpoint took 2.1 seconds. Fixed with a single batched query, it dropped to 80ms.

No linter catches this. No static analysis catches this. A slow endpoint in your APM dashboard gives you no cause. You have to see the query pattern at runtime.

2. SELECT * on a hot path

The authors query returns 23 columns. The endpoint only renders id, name, and bio. Every request is pulling avatar_url, created_at, updated_at, bio_short, social_links (JSON), and 17 other fields — for nothing.

This one is easy to miss because the query is "fast" in isolation. At scale it matters.

3. Heap growth

Steady heap increase with no GC plateau means something is accumulating and not being released — an unbounded cache, event listeners not being cleaned up, or a closure keeping references alive longer than expected. The agent tracks heap size per GC cycle and flags when the trend is consistently upward with no plateau.

4. Event loop lag

48ms event loop lag means the main thread is doing synchronous work somewhere in the request path. The baseline for a healthy Node.js server should be under 10ms. Above that, something is blocking the loop — a heavy computation, a synchronous file read, or a large JSON parse happening inline instead of in a worker.


The 2-line setup

npm install argus-apm
Enter fullscreen mode Exit fullscreen mode
import { ArgusAgent } from 'argus-apm';
await ArgusAgent.createProfile({ environment: 'dev', appType: ['web', 'db'] }).start();
Enter fullscreen mode Exit fullscreen mode

Drop this at the top of your entry file and run your app under normal load for a few minutes. Findings print to the console automatically in dev mode.

No account. No cloud. No config file. No side effects in production (the dev profile disables console output and sampling is 100% regardless of environment).


How it works

The agent hooks into node:diagnostics_channel — Node's official observability primitive, stable since Node 18. No monkey-patching, no prototype pollution, no changes to your existing code beyond the 2-line setup.

For N+1 detection: it tracks query fingerprints per HTTP transaction. If the same fingerprint fires repeatedly from the same call site in one request window, it flags it with file and line number.

For privacy: SQL values are destroyed at the AST layer before any metric is recorded. Not redacted from logs — shredded before they exist as data.

Supported drivers: pg, mysql2, mongodb, redis, ioredis, prisma, typeorm, sequelize, mongoose, sqlite3, better-sqlite3, mariadb, cassandra-driver, tedious, oracledb.


Try it on your app

npm install argus-apm
Enter fullscreen mode Exit fullscreen mode

Point it at a real project — not a hello world. Run it for 5 minutes under actual traffic. See what it finds.

What did it catch in your codebase? Drop your result in the GitHub Discussion — driver, framework, what it found. Even "found nothing" is useful data and takes 30 seconds to post.

Share what Argus found →

GitHub repo

Top comments (0)