All posts
No-Code & Tools

Your Images Might Be Hitting D1: How Asset Requests Quietly Burn Row-Read Quota

A page with 20 images can quietly create 20+ D1 reads when every asset request looks up metadata, ownership or an R2 key in the database. The media layer may be the hidden database workload.

Elvis Onunwa02 October 20267 min readLast updated: 02 October 2026
Your Images Might Be Hitting D1: How Asset Requests Quietly Burn Row-Read Quota

TL;DR

  • If an image URL is served through a Worker route that looks up asset metadata in D1, every browser image request can become a database request.
  • Responsive variants, crawlers, repeated page loads and cache misses can multiply those reads far beyond the number of HTML page requests.
  • Use stable object keys/URLs, edge caching and database-free public asset paths where authorization is not required; reserve D1 lookups for genuinely protected or dynamic asset decisions.
  • Trace asset endpoints separately in your D1 query analytics because their SQL may look unrelated to the page loader that triggered the images.
On this page⌄

You optimize your page loader.

The SSR route makes only a few Cloudflare D1 queries.

The dashboard still shows far more database activity than the page traffic seems to justify.

Look at the images.

I have audited a Cloudflare application where media delivery was part of the D1 problem because asset requests were not “just files.” Each image route could perform database work to resolve metadata or an object-storage key before returning the image.

That changes the workload completely.

A page with 20 images can become:

1 HTML request + 20 asset requests + 20 database lookups.

Add responsive variants, bots, social previews, cache misses and repeat navigation, and your media layer can quietly consume a significant share of D1 rows.

Why this architecture happens

A common upload system stores:

  • binary file in R2;
  • metadata row in D1;
  • application exposes /media/:assetId;
  • Worker receives the request;
  • Worker queries D1 for the asset row;
  • row contains R2 object key/MIME/ownership;
  • Worker reads object from R2 and returns it.

This design is understandable.

It gives the application one stable internal asset ID and lets the database own metadata.

The problem is not the design itself.

The problem is making the same D1 resolution mandatory for every public file request forever.

Browser subresources multiply differently from page views

Suppose the homepage includes:

  • hero image;
  • 12 story thumbnails;
  • logo;
  • 5 representative photos;
  • two decorative assets.

That is 21 image requests.

If every one performs a D1 point lookup, 1,000 homepage views can create roughly 21,000 additional database queries before considering browser caching or variants.

If the lookup is indexed and scans one row, that may still be manageable.

If the query scans multiple rows or performs ownership joins, the rows-read effect grows faster.

The important point is that HTML traffic is not asset traffic.

Do not estimate database load only from route views.

Responsive images can multiply one logical image

Modern image markup may request different sizes depending on device and layout.

Browsers usually choose one candidate from srcset, but other systems may request additional forms:

  • preload;
  • social crawler/Open Graph fetch;
  • admin preview;
  • image optimizer/proxy;
  • thumbnail generation;
  • transformed variants.

If every variant starts by resolving the same asset record through D1, one original upload can create many metadata lookups over its lifetime.

The database is now in the hot path for a resource that is otherwise highly cacheable.

Public assets should usually have a database-free fast path

For truly public media, I prefer a serving path that can be resolved without a live relational query on every hit.

Options include:

Stable R2/object key in the URL

If the public URL safely maps to the object key, the Worker can read R2 directly.

Immutable generated asset URL

Store a public path when the asset is published so rendering code does not need to resolve an internal ID repeatedly.

Edge-cached asset response

Let Cloudflare cache the media response so repeat requests do not reach your Worker/D1 path.

Static generated derivatives

For featured images, thumbnails or known public assets, generate them ahead of time and serve by deterministic path.

The right model depends on access/security requirements.

The principle is:

do not require relational state for a request whose answer rarely changes.

Protected media is different

Some assets genuinely require authorization:

  • private documents;
  • KYC uploads;
  • paid downloads;
  • tenant documents;
  • admin-only media.

Do not remove the permission check just to reduce D1 usage.

Instead, make the protected path explicit.

Potential approaches include:

  • signed short-lived URLs;
  • cached authorization where safe;
  • indexed point lookups;
  • separate public/private buckets or namespaces;
  • capability tokens;
  • gateway route that verifies once and issues temporary access.

Security is a constraint, not an optimization target to delete.

Metadata lookup can be cheap if the key is truly indexed

If you must query D1 per asset, make the query bounded.

For example:

SELECT r2_key, mime_type, visibility
FROM media_assets
WHERE id = ?
LIMIT 1;

with id as a primary/indexed key is fundamentally different from:

SELECT ...
FROM media_assets
JOIN users ...
JOIN projects ...
WHERE some_nonindexed_field = ?

The first can be close to a point lookup.

The second may scan or join much more data per image request.

Use D1 query analytics and EXPLAIN QUERY PLAN rather than assuming “it’s only one metadata row.”

Watch for authorization repeated on every public image

An especially wasteful pattern is applying private-style authorization to media that is already intentionally displayed on a public page.

For example:

  1. public homepage renders an event representative photo;
  2. browser requests /api/assets/123;
  3. route queries asset row;
  4. queries related content/site record;
  5. decides yes, this public image can be seen;
  6. reads R2.

The page already published the image as public, but every viewer re-proves that fact.

A better model may persist an explicit visibility/publication state and create a cacheable public URL once approved.

Asset existence checks can also become D1 work

Some media systems validate on every render:

  • is the asset still active?
  • is the linked record deleted?
  • does the R2 object still exist?
  • is the site logo configured?

Those checks are useful during mutation/admin operations.

They do not necessarily belong in every public request.

Validate strongly when the asset is attached/published. Serve cheaply afterward unless a business rule requires real-time revocation.

I use four traces.

1. Browser network trace

Count image/media requests for one page load.

2. Worker route trace

Identify which endpoint serves each asset.

3. D1 query trace

Find SQL statements executed by that media endpoint and measure rows/execution.

4. Cache trace

Check whether repeated requests are edge cache hits or repeatedly reaching application code.

This tells you whether the problem is:

  • too many assets;
  • missing edge cache;
  • per-asset D1 lookup;
  • expensive metadata SQL;
  • authorization fan-out;
  • cache headers that prevent reuse.

A simple before/after architecture

Before:

Browser -> /media/abc
        -> Worker
        -> D1: resolve abc -> r2/path.jpg
        -> D1: permission/metadata
        -> R2
        -> response

For public media, after can look like:

Browser -> /images/content/abc.webp
        -> edge cache
        -> R2/origin only on miss

D1 remains involved when publishing/managing the asset, not necessarily when every visitor views it.

Do not confuse storage cost with row-read cost

Moving binary files to R2 does not automatically remove D1 from the serving path.

You can correctly store images in R2 and still burn D1 quota if every R2 access first requires a database lookup.

That is why the architectural question is:

Where is the mapping from public request to object key resolved?

If the answer is D1 every time, your object-storage traffic has a database multiplier.

This also improves resilience

Removing D1 from public media delivery has another benefit.

If D1 becomes constrained or temporarily unavailable, cached/public assets can still load.

That reduces blast radius.

A database incident should not necessarily turn every logo and image into a broken placeholder.

This is the same resilience principle behind cache-first public SSR: dependencies should fail according to the importance of the data they provide.

The practical fix list

If image routes are showing up in your D1 workload:

  1. classify assets as public or protected;
  2. map public assets to stable cacheable paths;
  3. keep binary data in R2/object storage;
  4. avoid D1 resolution on repeat public hits where possible;
  5. add appropriate edge/browser caching;
  6. make required D1 metadata lookups primary-key/index bounded;
  7. remove repeated public authorization joins;
  8. validate asset relationships during admin/publish mutations;
  9. measure browser subrequest count, not only page views;
  10. recheck D1 rows/hour after the media change.

The media system is part of your database architecture even when the files themselves live elsewhere.

If your D1 row-read numbers still look impossible after optimizing your obvious page queries, inspect your subresources. And if you want help tracing the whole request graph—from SSR to D1 to R2—see my MVP development service or contact me.

Sources checked

Frequently asked questions

Questions, answered.

Can images cause Cloudflare D1 rows read?

Yes. If your image or media endpoint queries D1 to resolve an asset ID, metadata record, R2 key, permissions or ownership before serving the file, each asset request can consume D1 rows.

Should I store images in D1?

D1 is a relational database, not object storage. Store binary media in an object store such as R2 and keep only the metadata you actually need in D1.

How do I stop image requests from hitting D1?

For public media, prefer stable object keys or immutable public asset URLs that can be cached at the edge without resolving through D1 on every request. If access is protected, cache safe authorization/metadata decisions where appropriate and keep queries indexed/bounded.

Why are asset reads hard to spot?

A page loader may issue only a few database calls, while the browser independently requests many images after HTML renders. Those requests can hit a different Worker route and run their own SQL, so page-level tracing must include subresources.

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.