Insights / Technical notes

Separate Edge Caching for Public Images from API Rate Limits

A case where the content API and image requests shared one rate limit during repeated browsing. Covers reuse of public images, validation of successful responses, the WAF and application boundary, and production checks.

  • Cloudflare
  • Performance
  • Web
Separate Edge Caching for Public Images from API Rate Limits
Table of contents
  1. Reproduce consecutive viewing starting with one image
  2. Content and images shared the same rate-limit bucket
  3. Reuse only public images that can be shared
  4. Store only validated 200 responses
  5. Handle dynamic API limits independently
  6. Verify image content in production

In image-heavy journals and catalogs, changing the date or page triggers requests for both the content API and images. We describe a case where images stopped loading during repeated browsing, without exposing operational URLs, internal routes, or limit values.

Reproduce consecutive viewing starting with one image

Fetch the same public image repeatedly and compare body hash, status, and cache state. Then load text and multiple images through normal page changes and check which requests count toward the limit. A HIT alone is insufficient: test image correctness and API protection together.

Cloudflare Cache API:Conditional retrieval and cache locality

Content and images shared the same rate-limit bucket

In this case, requests to a dynamic API and GET requests for public images were counted by the same WAF rate-limit rule. Even ordinary page changes could trigger several requests at once, so the content could load while images were rate-limited.

Adding an edge cache for images does not help requests that the WAF blocks before they reach it. We made two separate changes: reducing load on the origin, and changing which requests count toward the shared rate limit. Existing application-level limits and quotas for generation remain in place for their respective purposes.

Reuse only public images that can be shared

The images covered here are immutable: the same public asset ID always returns the same content. We validate the asset ID format, request boundary, and Service Binding configuration before looking up a cache entry keyed by the same host and asset ID. A warm cache does not bypass these entry checks.

Query strings that do not affect the content and end-user request headers do not split the cache entry for the same image. This is safe only because these are immutable public images. The same approach cannot be applied unchanged to private images whose content varies by user or organization.

Store only validated 200 responses

On a cache miss, the image is fetched from a private Service Binding. We validate the HTTP status, image Content-Type, Content-Length, and response body, and store only a 200 response that meets those conditions. Partial responses, empty bodies, invalid metadata, and failure responses are not stored.

Storing the image body is separate from returning a 304 when the ETag matches. Cache writes are scheduled with waitUntil, and cache read or write failures must not prevent a valid image fetched from the origin from being returned. If a later request can use the cache, the Service Binding fetch can be skipped.

The Cloudflare Cache API documents conditional requests using ETag and the per-data-center nature of the cache. A HIT in one data center does not mean every data center has a HIT. Pages Functions response headers cannot be configured using only the _headers for static files; set them in the Function.

Handle dynamic API limits independently

Protecting the dynamic API is a separate requirement from caching images. In this case, public-image GET requests were excluded from the WAF aggregation, and after applying the change we read back the targeted dynamic APIs, limit period, action, and enabled state. We also saved the previous configuration for rollback.

Choose thresholds based on the number of requests generated by normal browsing and the load of the operation being protected. The plan-specific conditions for Cloudflare rate limiting also need review. A value in a repository operations note does not prove that a production rule is enabled.

Public images and dynamic API Design reuse for immutable public images separately from protection for dynamic APIs. Private images are out of scope.
  1. Public image GET Check the request boundary, then reuse the same image from cache. Store only validated 200 responses.
  2. Dynamic API Protect API work with WAF and the app quota. Its limits are separate from image caching.

Verify image content in production

Unit tests covered reuse of the same public image, separation by host and asset ID, boundary checks before cache use, 304 responses, cache failures, and responses that must not be stored. After CI, we confirmed production deployment through a GitHub push and the custom domain, then compared the HTTP result for representative images, the HIT or MISS state reported by the application, and the hash of the retrieved bytes.

In production, we also fetched the content and several images consecutively and confirmed that the requests were not rate-limited under that normal-browsing test condition. This was a check of a limited request pattern, not a test of the boundary under high load.

A cache-state display alone does not prove that the correct image was returned. We verify content retrieval, image retrieval, WAF configuration, and the viewing result in the UI separately. This record confirms delivery of representative images; it does not demonstrate continuous browsing for every user and data center or performance gains under high load.

For the division between static and dynamic site behavior, see the overall Astro and Cloudflare design. For delivery optimization of images, CSS, and other assets, see Astro performance tuning.