ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-07-10Advanced

An Agent's ORM Code Made p95 Five Times Slower — Measuring Query Counts and Blocking Them in CI

N+1 queries slip past code review because the code looks correct. Here is a CI gate that measures query counts and judges them by their slope against input size, with working code and real numbers.

Antigravity319Performance5ORMCI5Testing

Premium Article

Late one night I noticed the p95 latency on a dashboard had jumped to 940ms. The day before, that same list endpoint had been sitting comfortably at around 180ms.

Nothing was broken. Error rate was zero. The response bodies were correct. It was simply slow.

Tracing back, I landed on a small change I had handed to an agent that afternoon: add "the latest comment for each item" to the list response. Twelve lines of diff. When I reviewed it, my only thought was that it read nicely, and I merged it.

One of those twelve lines resolved a relation inside a loop.

Correct code is not necessarily fast code

What makes N+1 queries so treacherous is that nothing about them is wrong.

Types check. Tests pass. The JSON matches expectations. What a reviewer sees is await getComments(item.id) inside a for loop — a line that is, in isolation, perfectly reasonable. The fact that the number of calls scales with the request payload appears nowhere in the diff.

Since I started delegating implementation work to agents, this asymmetry has grown sharper. Where a human writing the loop might feel a small internal alarm, generated code always reads calm and correct. I have noticed that the shorter the diff, the more readily I let my guard down.

So I stopped trying to solve this by reviewing more carefully. That path depends on human attention, and human attention is not a system property.

You cannot protect what you do not measure.

Making query counts readable from a test

Every major ORM exposes a hook on query execution. That is all we need to count queries from inside a test.

With Prisma, instantiate the client with query events and push into a counter.

// test/support/query-counter.ts
import { PrismaClient } from "@prisma/client";
 
export const prisma = new PrismaClient({
  log: [{ emit: "event", level: "query" }],
});
 
let count = 0;
const seen: string[] = [];
 
prisma.$on("query", (e) => {
  count += 1;
  seen.push(e.query);
});
 
export function resetQueryCount(): void {
  count = 0;
  seen.length = 0;
}
 
export function getQueryCount(): number {
  return count;
}
 
export function getQueries(): readonly string[] {
  return seen;
}

With Drizzle, swap in a custom logger.

// test/support/drizzle-counter.ts
import { drizzle } from "drizzle-orm/node-postgres";
import type { Logger } from "drizzle-orm/logger";
 
class CountingLogger implements Logger {
  count = 0;
  logQuery(query: string): void {
    this.count += 1;
  }
}
 
export const logger = new CountingLogger();
export const db = drizzle(pool, { logger });

With SQLAlchemy, listen on before_cursor_execute.

# tests/support/query_counter.py
from contextlib import contextmanager
from sqlalchemy import event
 
@contextmanager
def count_queries(engine):
    counter = {"n": 0}
 
    def _on_execute(conn, cursor, statement, params, context, executemany):
        counter["n"] += 1
 
    event.listen(engine, "before_cursor_execute", _on_execute)
    try:
        yield counter
    finally:
        event.remove(engine, "before_cursor_execute", _on_execute)

That is only instrumentation. The harder question was what to assert on.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Turning query counts into a testable value, and detecting N+1 by slope against input size
Hook implementations for Prisma, Drizzle, and SQLAlchemy, plus the full CI wiring
Measured outcomes and honest limits: p95 940ms to 190ms, gate cost +11 seconds
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-07-06
Building a Live Wallpaper That Doesn't Drain the Battery — Designing the WallpaperService Render Loop Around a Power Budget
Live wallpapers quietly burn battery because they keep drawing when nothing is visible. Here is the WallpaperService.Engine design I used to cut power draw by roughly 60% in a real wallpaper app, built around visibility, idle, and thermal gates.
App Dev2026-06-24
Running Pre-Release Checks Without Opening the IDE — Designing the Android CLI as the Verification Gate of an Unattended Pipeline
How to slot Android CLI v1.0 into an unattended pipeline as its verification gate — three layers of checks, an exit-code contract, and a density-by-locale matrix, sized for an indie developer's day-to-day.
App Dev2026-03-19
Advanced C# Refactoring in Unity with Antigravity — Performance Optimization Implementation
Master advanced refactoring techniques for Unity C# using Antigravity. Reduce GC allocation by 78%, implement ECS patterns, leverage JobSystem, and optimize with Burst compiler. Includes production benchmarks.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →