Hydrate
the World
AI agents reach for the broadest helper they can find. They call listEverything() to populate a dropdown. They serialize
forty-field objects when the screen needs three. The page works
perfectly with twelve rows — and dies the moment real data shows up.
It's a specific failure mode and it shows up everywhere. The detail loader fans out into eight serial queries. The contacts list pulls every tag, every note, every file, every permission — for a screen that renders a name and a status badge. The search box loads the whole table and filters in JavaScript.
None of it shows up in code review because it all works. It only breaks when the customer migrates their real data in. This is a structured audit for that exact failure mode.
The full audit prompt is at the bottom. Copy it. Run it.
Ten Smells, One Pattern
Every one of these comes from the same root cause: the agent generated each piece of code in isolation and never asked whether the caller actually needed all of it.
The God List
listCompanies(), listContacts(), listDeals(), listUsers() called from a page loader to populate a dropdown, label, or count.
The helper returns every row with every field. The screen needs id and name. Works fine at 12 rows. Falls over at 12,000.
The Serial Waterfall
A detail loader awaiting five related queries one after the other when none of them depend on each other.
AI writes top-to-bottom. Each await is invisible latency that compounds. Five 60ms queries become 300ms instead of 60ms.
The N+1 Map
rows.map(async row => fetchDetails(row.id)) — a per-row DB or API call hidden inside an array iteration.
A list of 50 becomes 51 round trips. The agent reasoned about one row at a time and never noticed the loop turned it into a fan-out.
The Unfiltered Query
SQL in a request path with no WHERE, no LIMIT, no projection. SELECT * FROM contacts in a route handler.
AI optimizes for "returns the right shape." It does not model the size of the table or who is allowed to see it.
The Always-On Bundle
Helpers like getCompanyDetail that always join tags, counts, addresses, activities, notes, files, permissions.
One caller needed all of it once. Now every caller pays for it forever — including the autocomplete that just needs a name.
The Decorative Payload
A UI component that destructures three fields from a forty-field response. The other thirty-seven traveled the wire for nothing.
AI generates the API and the component independently. Nobody trims the response to match what the screen actually renders.
The Infinite Page
API endpoint that returns the entire table. Pagination, if it exists, happens in the client after the payload arrives.
The demo had 20 rows. Production has 200,000. The first user with real data takes down the page.
The Bootstrap Loop
Schema, config, feature flags, or org metadata fetched on every request instead of cached.
AI wires a getConfig() call into the loader and never asks if it could be cached. Every request pays the cost.
The Client-Side Filter
Search box or filter UI that loads all records up front, then filters with JavaScript.
Server-side filtering is more code. AI reaches for the path of least resistance and ships the version that works at demo scale.
The Detail Frenzy
get*Detail() returning permissions, activities, audit log, related records — for a route that renders only the title and status.
The detail helper is a dumping ground. Every screen that touches the entity drags the whole payload along.
Why AI Over-Fetches by Default
Over-fetching isn't a bug AI introduces by accident. It's the natural output of how AI agents reason about code. Once you see the pattern, you can predict exactly where it'll show up next.
It Reaches for the Broadest Helper
When the agent needs some data about contacts, it calls listContacts(). That helper exists, it returns the
right type, the code typechecks. The fact that it returns every
field on every row never enters the conversation.
It Writes Awaits Top to Bottom
Five queries get five awaits in sequence. Promise.all requires the agent to look at all five at once and notice they're
independent. It rarely does. Latency adds up silently.
It Reasons About One Row at a Time
rows.map(async r => fetchDetails(r.id)) looks fine
when you read it as "for each row, get the details." It doesn't
look fine when you read it as "fan out to N parallel database
queries." The agent reads it the first way.
It Ships What Works at Demo Scale
The demo had twelve contacts. The agent never saw the version with twelve thousand. Pagination, projection, server-side filtering — these are all extra code the agent has no incentive to write when the simpler version passes the same tests.
How to Rank Findings
Not every over-fetch matters. The audit prioritizes by what hurts at scale, not by what looks suspicious in isolation.
Loads hundreds or thousands of rows, or fans into N+1
Request paths that pull unbounded result sets, detail loaders that issue per-row queries, search endpoints that ship the whole table. These get worse linearly (or worse) with data growth.
Over-fetching that production data will expose
Helpers that always pull tags, notes, activities. Detail responses with thirty fields the UI ignores. Nothing breaks today — every customer migration makes it slower.
Real but bounded
Admin-only screens, internal tools, paths with natural row limits. Worth fixing eventually. Not what you ship to production users first.
Fastest Fixes
Six moves that resolve the vast majority of over-fetching findings. Match the fix to the smell — most aren't refactors, they're one-file edits.
Lightweight projection
Add an explicit field list (or a slim variant: listContactsBrief) to broad helpers used for dropdowns and labels.
Server-side pagination
Push LIMIT, OFFSET, and cursor handling down to the query. Never rely on the client to slice the result.
Promise.all the waterfall
Independent awaits in a loader become a single Promise.all. Free latency reduction with zero behavior change.
Batched lookup
Replace rows.map(async ...) with one IN-clause query or a single batched API call keyed by id.
Cached bootstrap
Schema, config, feature flags belong in module-scope cache or a request-scoped memo — not on the hot path.
Separate endpoint
When two callers want different shapes from the same entity, give them two endpoints. Stop overloading getDetail.
Copy It. Run It.
Paste this into Claude Code, Cursor, or any agent with codebase access. Point it at your project. It'll find the over-fetching and rank it for you.
# Hydrate the World — Over-Fetching Audit You are reviewing this codebase for "hydrate the whole world for one screen" performance smells. Goal: find request paths, loaders, API handlers, UI page data loaders, and server helpers that fetch or serialize much more data than the screen or caller actually uses. ## Look for - Page loaders/controllers calling broad helpers like `listCompanies`, `listContacts`, `listDeals`, `listUsers`, `get*Detail`, or unfiltered `list*`. - Full entity summaries used only for dropdowns, labels, counts, links, or search indexes. - Detail loaders that call many related queries serially when they could run in parallel. - N+1 patterns: `rows.map(async row => ...)` with per-row DB/API lookups. - SQL in request paths without appropriate `WHERE`, `LIMIT`, pagination, or projection. - Helpers that always fetch tags, counts, addresses, metadata, activities, notes, files, permissions, etc. even when most callers need only `id/name/status`. - UI components that only read a few fields from a much larger returned object. - API responses that expose large arrays or nested relations without pagination. - Repeated schema/bootstrap calls in hot request paths. - Search/filter screens that load all records client-side when server-side filtering or pagination is needed. ## For each finding, report 1. File and line. 2. Route/helper/function involved. 3. Why this is over-hydrating. 4. What fields the caller appears to actually use. 5. Likely impact as data grows. 6. Suggested fix: lightweight query, filtered helper, projection option, pagination, `Promise.all`, batched lookup, cached lookup, or separate endpoint. ## Prioritize - **HIGH:** request path can load hundreds/thousands of rows or perform N+1 DB/API calls. - **MEDIUM:** over-fetching likely noticeable with migrated/production data. - **LOW:** inefficiency is real but bounded or admin-only. Do not rewrite code unless asked. Produce a ranked audit with concrete examples and a short "fastest fixes" section. --- ## Report template ```markdown # Over-Fetching Audit: [Project Name] **Date:** YYYY-MM-DD **Stack:** [e.g., SvelteKit, Postgres, Drizzle] **Scope:** [routes/helpers reviewed] ## Summary Found X high, Y medium, Z low findings. [One paragraph: which patterns dominate, where the worst over-fetching lives, which fixes give the most leverage.] ## Findings ### [HIGH] Title — concise description **Location:** `path/to/file.ts:line` **Caller:** route/loader/component name **Issue:** What is being fetched and why it is more than the caller needs. **Caller actually uses:** [list of fields the screen or downstream code reads] **Impact at scale:** What happens when the underlying table reaches 10k, 100k, or 1M rows. Be specific about request time, payload size, and N+1 multipliers. **Suggested fix:** [lightweight query / pagination / Promise.all / batched lookup / cached lookup / separate endpoint] — concrete enough that an engineer can implement it without re-reading the code. --- [Repeat for each finding] ## Fastest fixes Numbered list of the highest-leverage changes, ordered by effort-to-impact. A reader should be able to scan this and know what to do first. ```
Want a professional review?
The prompt catches the predictable stuff. A human catches the rest. Book a conversation and we'll run a full performance audit on your project — over-fetching, query plans, payload sizes, the whole stack.