Overfetching is an API problem where responses carry more data than the client needs; underfetching forces extra requests to get enough. They are the twin failure modes of fixed response shapes — one wastes bytes, the other wastes round trips — and most APIs commit both on the same screen: each response too fat, and too many of them.
Key takeaways
| Question | Answer |
|---|---|
| Overfetching | Too much per response — bandwidth, parsing, battery, exposure |
| Underfetching | Too little per response — extra round trips, waterfalls, N+1 |
| The root cause | Fixed endpoint shapes meeting screens with different needs |
| REST fixes | Sparse fieldsets · include/expand params · pagination · composite endpoints |
| GraphQL’s answer | Selection sets — with honest caveats at the resolver layer |
One screen, three ways to fetch it
A post list that renders each title with its author’s name:
Overfetching Underfetching
GET /posts GET /posts (author IDs only)
→ 20 posts × 40 fields GET /users/11 ┐
→ ~160 KB shipped, GET /users/12 │ 20 more calls —
~6 KB rendered (96% waste) … │ the N+1 waterfall
GET /users/30 ┘
The shaped query
GET /posts?fields=title,summary,author&include=author&limit=20
→ 20 posts × 3 fields + their authors — one round trip, ~7 KB
The same shaped query as SDK code — projection and relation in one request:
// JavaScript / Node.js — Back4app JS SDK
// One shaped query: no overfetch, no underfetch
const query = new Parse.Query('Post');
query.select('title', 'summary', 'author'); // only what the screen renders
query.include('author'); // related object, same response
query.limit(20); // bounded page
const posts = await query.find();
// 1 round trip — not 1 list call + 20 author calls (N+1) // Flutter / Dart — Back4app Flutter SDK
// One shaped query: no overfetch, no underfetch
final query = QueryBuilder<ParseObject>(ParseObject('Post'))
..keysToReturn(['title', 'summary', 'author']) // only what the screen renders
..includeObject(['author']) // related object, same response
..setLimit(20); // bounded page
final response = await query.query();
// 1 round trip — not 1 list call + 20 author calls (N+1) // iOS / Swift — Back4app Swift SDK
// One shaped query: no overfetch, no underfetch
let query = Post.query()
.select("title", "summary", "author") // only what the screen renders
.include("author") // related object, same response
.limit(20) // bounded page
query.find { result in
// 1 round trip — not 1 list call + 20 author calls (N+1)
if case .success(let posts) = result { render(posts) }
} // Android / Kotlin — Back4app Android SDK
// One shaped query: no overfetch, no underfetch
val query = ParseQuery.getQuery<ParseObject>("Post")
query.selectKeys(listOf("title", "summary", "author")) // only what the screen renders
query.include("author") // related object, same response
query.limit = 20 // bounded page
query.findInBackground { posts, e -> if (e == null) render(posts) }
// 1 round trip — not 1 list call + 20 author calls (N+1) What is overfetching?
Overfetching is the API-layer version of SELECT *: the endpoint returns its full fixed representation regardless of what the caller renders. The costs stack in layers. The server serializes fields nobody reads; the network carries them — which is where mobile suffers, since transfer time scales with bytes over constrained bandwidth and every needless kilobyte spends metered data and radio battery; then the client parses the whole thing, because decompression happens before rendering and a bloated response is bloated parse work even when compression hid it on the wire.
The quieter cost is exposure. A response field the UI never shows is still one developer-tools click from being read — internal flags, other users’ email addresses, margin data. The OWASP API Security Top 10 tracks this as excessive data exposure (broken object property level authorization): least privilege applies to response bodies, and a field that no client should see should never be serialized in the first place.
What is underfetching?
Underfetching is the opposite deficiency: the endpoint’s fixed shape carries too little, so the client becomes an integrator — fetch the list, then fetch each item’s author, then maybe each author’s avatar. Every extra call is a full round trip, and round trips are the currency mobile networks are poorest in: at a realistic 100 ms per request, a 20-item list resolved sequentially spends two seconds on latency alone, before a byte of payload math.
At scale this waterfall has a name — the N+1 request problem: one call for N items, N calls for their details. The shape is fractal; it recurs wherever a fixed interface meets relational data — HTTP clients against REST endpoints, GraphQL resolvers against the database, ORMs lazy-loading their way through a loop — and the cure is always some form of batching the N into one.
Overfetching vs. underfetching
| Overfetching | Underfetching | |
|---|---|---|
| Symptom | Responses full of unrendered fields | Screens assembled from many calls |
| Unit of waste | Bytes (and parse time) | Round trips (and latency) |
| Worst on | Metered, slow, battery-bound networks | High-latency networks — waterfalls compound |
| Detection | Compare fields returned vs. fields rendered | Count requests per screen in the network tab |
| Direct fix | Sparse fieldsets / projection | Expansion params, composite endpoints |
| Escalation | Excessive data exposure (security) | N+1 request storms (scale) |
Diagnosis is mercifully mechanical, and no ranking explainer says so: open the network tab on one screen. Many requests for one view is underfetching; large responses whose fields you can’t find in the UI is overfetching. Endpoint analytics generalize the audit — p95 payload size per endpoint, requests per session per screen.
Fixing both without leaving REST
The GraphQL migration is not the first resort; mature REST conventions cover most of the gap:
- Sparse fieldsets — a
fieldsparameter that projects the representation: standardized as JSON:API sparse fieldsets, mirrored by$select-style query options and SDKselect()builders. The overfetching fix at the source. - Expansion parameters —
include=author,commentsembeds related resources in the same response (compound documents), converting an N+1 waterfall into one request. The underfetching fix at the source. - Pagination — bounds the list dimension of overfetching; unbounded collections are payload bugs that grow with adoption.
- Purpose-built and composite endpoints — when one screen always needs the same aggregate, give it an endpoint that returns exactly that aggregate, assembled server-side where latency between services is microseconds, not mobile round trips.
- A backend-for-frontend — the architectural version of the same move: a thin per-client layer that speaks generous internal APIs and serves each frontend exactly its shape (Sam Newman’s BFF pattern).
- Compression — honest last place: it shrinks the wire, not the waste; parse cost and exposure survive it intact.
Does GraphQL solve it?
Mostly — and the “mostly” is worth knowing. Selection sets make the client’s field list the request itself, which retires classic overfetching, and nested queries assemble related data in one round trip, which retires classic underfetching. That is exactly why the GraphQL vs. REST debate starts with these two words.
The caveats live one layer down. Clients that copy-paste generous queries overfetch by habit — nothing enforces that a query matches what a component renders unless the team adopts per-component fragments. And a naive resolver chain underfetches against the database: a query for 20 posts with authors becomes 1 + 20 database reads unless resolvers batch through a loading layer — the same N+1, relocated. GraphQL moves the problem to a layer you control, which is genuine progress; it does not delete it.
Common use cases
Where the two problems (and their fixes) show up first:
- Mobile list screens — the canonical overfetch: full rows shipped to render three fields per cell.
- Detail screens with relations — post + author + comments: underfetch waterfalls unless expanded or composite.
- Slow-network markets — both problems taxed at the highest rate; shaped queries as accessibility.
- Dashboards — aggregate screens that either overfetch raw rows or underfetch across five services; BFF territory.
- Public APIs with diverse consumers — one fixed shape cannot fit a watch face and an admin console; projection and expansion parameters let each caller tune.
Which problem do you have? A decision matrix
| Network-tab evidence | Diagnosis | First fix |
|---|---|---|
| One request, big response, few fields rendered | Overfetching | Sparse fieldsets / select() |
| Many sequential requests per screen | Underfetching | include / expansion params |
| Requests scale with list length | N+1 | Batch: expansion or composite endpoint |
| Both large and many | Both — common | Shaped query or GraphQL selection |
| Response fields you’d rather not see leave the server | Exposure | Trim serialization server-side, not client-side |
Limitations and trade-offs
- Projection couples clients to field lists. A
fieldsparam that drifts from the UI causes missing-data bugs; generated types and review keep selections honest. - Expansion can overcorrect.
include=commentson a hot list can ship megabytes of embedded relations — expanded responses need their own pagination and depth limits. - Purpose-built endpoints multiply. Per-screen endpoints fix fetching and create an endpoint-sprawl maintenance bill; BFFs concentrate that sprawl into an owned layer, at the cost of running one.
- Server-side flexibility has a price. Arbitrary projection and expansion complicate caching (each shape is a cache key) and authorization (every combination must be safe to serve).
- The problems are also modeling signals. A screen that needs deep trimming or five includes may be telling you the resource shapes are wrong — sometimes the fix is the data model, not the fetch.
Overfetching and underfetching on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Both fixes ship as query primitives on every SDK — the code tabs above are the whole pattern: select() is the sparse fieldset, include() is the expansion parameter, and together they turn a 1 + N waterfall into one shaped round trip against the auto-generated REST API. When clients want full control of response shape, the same data is queryable through GraphQL selection sets; when a screen needs a server-side aggregate, a Cloud Code function is a composite endpoint you write in one file. The fetch matches the screen — by idiom, not by endpoint redesign.
Frequently asked questions
What is overfetching?
An API returns more data than the client needs for the task at hand — a profile screen that renders three fields receives forty. The waste is paid four times: server serialization, network transfer, client parsing, and, on mobile, battery and metered data. It can also expose fields no client should see.
What is underfetching?
A single endpoint does not return enough data to render the screen, so the client makes additional requests to assemble it. Each extra call is a full network round trip; when the calls are sequential — fetch the list, then fetch details per item — latency compounds into the N+1 request problem.
What is the difference between overfetching and underfetching?
Direction. Overfetching means each response carries too much — the cost is bytes; underfetching means each response carries too little — the cost is round trips. Both grow from the same root: fixed response shapes designed once, consumed by screens with different needs. Many APIs manage both at the same time on the same screen.
Does GraphQL solve overfetching and underfetching?
Largely, at the HTTP layer: selection sets fetch only requested fields and nested queries gather related data in one request. But it is not automatic — clients that request generous field sets recreate overfetching, and naive resolvers recreate underfetching against the database as resolver-level N+1, which batching loaders exist to fix.
How do you avoid overfetching in a REST API?
Sparse fieldsets are the direct fix: a fields parameter (or select() in SDK query builders) that projects only the columns the screen renders. Pagination bounds list size, purpose-built endpoints match responses to real screens, and compression shrinks whatever remains — though compressing bloat is a mitigation, not a cure.
How do you fix underfetching without switching to GraphQL?
Expansion parameters — include or expand — that embed related objects in one response; compound documents that ship a resource with its associations; composite endpoints that aggregate one screen's needs server-side; and, architecturally, a backend-for-frontend layer that does the assembly close to the data instead of across a mobile network.
How does the N+1 problem relate to underfetching?
N+1 is underfetching at scale: one request for a list of N items, then N follow-up requests for the details of each. The same shape recurs at every layer — HTTP clients against REST endpoints, GraphQL resolvers against the database, ORMs lazy-loading relations — and the fix is always the same idea: batch the N into one.
Why is overfetching a security risk?
Fields a screen never renders still cross the network — and anyone can open developer tools and read them. Internal flags, email addresses, and cost data leak this way; security taxonomies classify it as excessive data exposure, and the principle of least privilege applies to response bodies just as it does to permissions.