All posts
No-Code & Tools

Why Cloudflare D1 Rows Read Explode: SSR Fan-Out, Duplicate Queries and Full Scans

High D1 row reads are often not one bad SQL statement. SSR loaders, duplicate hydration fetches, category fan-out and scans can multiply each other until a normal page view becomes dozens of database operations.

Elvis Onunwa02 October 20268 min readLast updated: 02 October 2026
Why Cloudflare D1 Rows Read Explode: SSR Fan-Out, Duplicate Queries and Full Scans

TL;DR

  • D1 usage can explode when one page render fans out into parent loaders, child loaders, image metadata lookups, recent-content queries and client hydration refetches.
  • Optimize both dimensions: how often a query executes and how many rows each execution scans.
  • In production audits I have found homepage category fan-out, related-content `EXISTS` scans, global ranking/window queries, correlated lookups and eager admin loaders dominating D1 usage.
  • Trace one user request end-to-end and give every database call a reason to exist before adding more caching or indexes blindly.
On this page⌄

A page can make one HTTP request from the visitor and still create dozens of database operations behind the scenes.

That is the pattern I call SSR fan-out.

I have investigated Cloudflare D1 applications where the database problem was not one spectacular query. It was the multiplication of several ordinary-looking behaviours:

  • root loader reads navigation/configuration;
  • child loader fetches page data;
  • a second helper fetches overlapping page data again;
  • related-content logic scans candidate rows;
  • each image requests metadata/authorization from D1;
  • category sections execute independently;
  • hydration refetches something SSR already loaded;
  • an admin route eagerly loads hundreds of records;
  • cache misses rebuild several snapshots at once.

Individually, each decision can look defensible.

Together, one page view becomes a database event.

If your D1 rows read are exploding, I would investigate multiplication first, not assume you simply need a bigger plan.

Two multipliers create the bill

D1 usage can grow from two different directions:

Multiplier 1: executions

How many times does SQL run?

A perfectly indexed query that scans five rows is still wasteful if a single page executes it 30 times unnecessarily.

Multiplier 2: scan cost

How many rows does each execution read?

A query that runs only 50 times can dominate usage if each execution scans thousands of rows.

The total effect is roughly:

executions × rows scanned per execution

You need to reduce the right side of that equation.

My D1 rows read vs query count guide explains the metrics themselves. This article is about where the multiplication comes from.

Root loader + child loader duplication

Modern SSR frameworks make nested routes convenient.

That can also make it easy to read the same data at multiple levels.

Imagine:

  • root loader fetches site configuration and current event;
  • /register loader independently fetches current event and pricing;
  • a shared RegistrationShell server helper fetches the event again;
  • a child step asks for country pricing and re-fetches the registration context.

The visitor sees one registration page.

D1 may see four overlapping reads before the browser becomes interactive.

In one application audit, the public SSR baseline was around a dozen SQL reads before considering all downstream media/admin behaviour.

The fix starts by drawing the loader tree and asking:

Which layer owns each piece of data?

If parent data is already available to a child, reuse it instead of re-deriving it from D1.

Hydration can fetch what SSR already fetched

A subtle duplication happens when server rendering fetches data and the browser immediately runs a client fetch for the same information after hydration.

This often appears in:

  • tickers;
  • breaking-news bars;
  • user/session widgets;
  • category rails;
  • pricing/configuration hooks;
  • “latest” components.

The developer may see the client request as harmless because it keeps data fresh.

At scale, every page view becomes:

SSR read + hydration read

for information that changed zero times between those moments.

We fixed one production component where a breaking-news/ticker path unnecessarily fetched again during hydration. Removing that duplicate request cut recurring database work without changing the UI.

Category fan-out

Homepages are dangerous because they aggregate many small content blocks.

A page may need:

  • 7 politics stories;
  • 7 business stories;
  • 7 sports stories;
  • 7 entertainment stories;
  • and so on.

One implementation might run a separate query for every category.

That is not automatically bad—bounded indexed queries can be very efficient.

The problem appears when every query scans a large candidate set or when the page rebuilds the whole structure far more often than needed.

In one measured window, category queries executed 34 times and read roughly 28,370 rows. Another recent-content statement executed just four times but scanned roughly 7,960 rows.

The right fix was not simply “make it one SQL query.” A single global ranking query later became its own hotspot.

The goal is bounded work, not minimum statement count.

Global ranking/window queries can hide enormous scans

A clever SQL query can generate top-N results for every category using a window function or global CASE ranking.

It looks elegant because the application makes one database call.

But the database may need to:

  1. scan a broad public-content set;
  2. partition/rank rows;
  3. sort candidates;
  4. return only a few rows per category.

I encountered a homepage snapshot rebuild where one WITH ranked ... style query accounted for roughly three quarters of the measured rows in that window.

We replaced the global ranking approach with small indexed top-7 queries per category, batched in one D1 round trip.

More SQL statements internally; dramatically less scanned work.

This is why “one query” is not a performance strategy.

Related-content logic often becomes expensive as the article table grows.

Common patterns include:

  • EXISTS against tag junction tables;
  • OR conditions across category/tag/topic;
  • scoring matches with CASE;
  • excluding the current content;
  • recency ordering;
  • fallback queries.

A four-card component can scan thousands of rows per request.

If that component appears on every article page, traffic multiplies the problem quickly.

One production query I measured consumed about 147,820 rows across 85 executions.

The eventual architecture moved related-content generation away from repeated live recomputation toward bounded snapshots with their own refresh lifecycle.

The user still gets related stories. D1 does not have to rediscover the same relationships on every page view.

Correlated lookups and N+1 work

N+1 is not limited to ORMs.

You can create it manually:

  1. fetch 20 stories;
  2. for each story fetch its category;
  3. for each story fetch its image metadata;
  4. for each story check some auxiliary state.

That can turn one list into 61 queries.

Sometimes a join or batched lookup is better.

Sometimes the data belongs in a snapshot or denormalized read model.

The important thing is to recognize the shape.

When debugging, group SQL by request/route rather than viewing every query as an independent event.

Image routes can quietly become database routes

A browser may request 20 images for one page.

If your asset endpoint performs a D1 lookup for every request to find:

  • metadata;
  • ownership;
  • MIME type;
  • authorization;
  • R2 key;

then the image grid is also a database workload.

Now add responsive image variants, social crawlers, cache misses and repeat visitors.

The visible page has no new text content, but D1 usage climbs.

I cover this separately in Your Images Might Be Hitting D1.

Eager admin loaders

Public traffic is not the only source.

An admin dashboard may load 250 records, statistics, media, categories and audit logs at once because “only admins use it.”

But one admin refreshing repeatedly can generate large row reads if those queries scan/sort unindexed tables.

Audit pages are particularly easy to neglect. A missing timestamp index on a log table can make a simple “latest 100 events” view increasingly expensive over time.

Internal pages deserve query budgets too.

Runtime schema/index checks

Another pattern is running schema or index introspection during normal request handling.

A development safety check can accidentally become production traffic:

  • does table exist?
  • does index exist?
  • has migration run?
  • inspect pragma/schema metadata.

Those checks belong in migration/deployment/health workflows where possible, not in every page request.

Production request paths should assume a deployed schema contract and fail clearly if that contract is broken.

Cache misses can stampede

Caching does not automatically save you if many requests miss at the same time.

Suppose a homepage snapshot expires every five minutes.

If 20 requests arrive immediately after expiry and each independently rebuilds the snapshot, your cache has multiplied the expensive query by 20.

Use one or more of:

  • deduped rebuilds;
  • stale-while-revalidate style behaviour;
  • short lock/lease;
  • background refresh;
  • last-known-good stale copy;
  • circuit breaker.

The best cache is not only a TTL. It is a controlled rebuild lifecycle.

How I trace one SSR request

I like to build a request budget.

For one route, record:

Layer Calls Rows read Why?
Root loader
Route loader
Shared helpers
Related content
Asset metadata
Hydration
Background rebuild

Then ask of every line:

  1. Does it need to happen for this request?
  2. Is this data already available?
  3. Can it be read from cache/snapshot?
  4. Is the SQL index-bounded?
  5. Can several calls be batched?
  6. Can rebuild work be deduplicated?

That exercise is usually more useful than adding random indexes based on table size.

Fix order matters

I normally prioritize:

1. Catastrophic scans

A query scanning thousands of rows every execution deserves immediate attention.

2. High-frequency duplicates

Hydration duplication, per-item lookups and nested-loader overlap can create easy wins.

3. Expensive rebuild paths

Snapshot/category/related-content refresh should be bounded and deduped.

4. Caching

Once the underlying query is sane, caching reduces how often it needs to run.

5. Admin/internal paths

They may be lower traffic but can still grow unbounded as data accumulates.

This sequence avoids caching an unnecessarily expensive query and calling the problem solved.

Treat database work as architecture, not an afterthought

The core lesson from these incidents was not “D1 is expensive.”

D1 can be extremely efficient when queries are bounded and the request architecture respects the edge/database boundary.

The problem was that application convenience had hidden multiplicative work.

One page request was not one database task.

Once we measured the actual fan-out, we could redesign the read path around cached snapshots, bounded queries and explicit ownership of data loading.

If your Worker looks healthy but D1 usage keeps climbing faster than traffic, contact me or see my MVP development service. I work on exactly this class of production issue: tracing the system from request to database rather than patching the loudest query in isolation.

Sources checked

Frequently asked questions

Questions, answered.

Why did my Cloudflare D1 rows read suddenly increase?

Common causes include a newly deployed query that scans more rows, SSR loader duplication, client hydration refetches, category or per-item query fan-out, missing indexes, eager admin queries, image routes that hit D1 and cache misses that cause expensive rebuilds.

Can one SSR page load run the same D1 query multiple times?

Yes. Parent and child route loaders, server helpers, metadata generation and client hydration can independently request overlapping data unless the framework/app deliberately shares or caches the result.

Should I fix high D1 usage with caching or indexes first?

Measure first. If one execution scans thousands of rows, fix query shape/indexing. If a cheap query executes dozens of times per page, remove duplication or cache it. Many real incidents require both.

What is SSR fan-out?

It is the multiplication where one incoming page request triggers many downstream database reads—for navigation, page data, related items, assets, categories, permissions or other components—so traffic at the edge becomes much larger traffic against D1.

NEED HELP WITH THE BUILD?

Turn what you learned here into a clearer website or MVP.

Share the goal, current stage, and constraints. I’ll help you identify the smallest responsible next step.

Continue reading

Related notes.