REST vs GraphQL Caching: CDN Strategies for Each API Style
REST and GraphQL represent fundamentally different philosophies about how clients request data, and those differences cascade directly into how CDNs cache their responses. REST's resource-oriented model, where each entity lives at a unique URL and responds to standard HTTP methods, was designed in an era when HTTP caching was the primary performance optimization. GraphQL's query-oriented model, where clients specify exactly the data they need through a single endpoint, was designed to solve the over-fetching and under-fetching problems that REST creates for complex, multi-resource UIs. This article examines how each API style interacts with CDN cache key design, TTL strategies, and invalidation patterns, providing concrete guidance for teams choosing between them or running both.
How REST Maps to CDN Caching
REST's caching story is the HTTP caching story. Each resource has a unique URL (/users/42, /products/789), GET requests are cacheable by default (RFC 7231), and the server controls caching behavior through Cache-Control, ETag, and Last-Modified headers. The CDN uses the URL as the cache key, respects the Cache-Control max-age directive for TTL, and revalidates stale entries using conditional requests.
// REST: each resource is a naturally cacheable URL
GET /api/products/789
Cache-Control: public, max-age=300, stale-while-revalidate=60
ETag: "a1b2c3d4"
// CDN cache key = URL = /api/products/789
// Cached for 300 seconds at edge
// Serves stale for 60s while revalidating in background
// 7231-compliant: any CDN handles this without configuration
GET /api/users/42
Cache-Control: private, no-store
// CDN: do NOT cache (user-specific data)
GET /api/categories
Cache-Control: public, max-age=3600, s-maxage=86400
// CDN caches for 86400s (s-maxage overrides max-age for shared caches)
The critical advantage is that no custom CDN configuration is required. A properly implemented REST API works with any CDN out of the box because it speaks the same HTTP caching language that CDNs were built to understand. The server sets the right headers, the CDN caches the right responses, and the client receives correct data at edge-network latency. ETags enable conditional requests that save bandwidth when the resource has not changed since the last fetch.
REST's Caching Challenges
REST's simplicity creates its own problems. The most significant is the N+1 problem: rendering a complex page requires fetching multiple resources from multiple URLs. A product page might need /products/789, /products/789/reviews, /products/789/recommendations, and /users/42 (the reviewer). Each request is independently cacheable, but the client makes 4 round trips to the CDN edge, and the responses may include data the client does not need (over-fetching).
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
Aggregate endpoints (/products/789?include=reviews,recommendations) solve the round-trip problem but complicate caching. The include parameter becomes part of the cache key, and every unique combination of included resources creates a separate cache entry. If the product data changes but the reviews have not, the entire aggregate response is invalidated even though only the product portion changed.
// REST: N+1 fetching problem
// Client needs: product details + reviews + recommendations
// 3 separate cacheable requests:
GET /products/789 → cached 300s
GET /products/789/reviews → cached 60s
GET /products/789/recs → cached 120s
// 3 round trips, but each independently cached and invalidated
// REST: Aggregate endpoint (solves N+1, complicates caching)
GET /products/789?include=reviews,recs
// Single round trip, but:
// - TTL = min(product_ttl, reviews_ttl, recs_ttl) = 60s
// - Product change invalidates entire aggregate
// - Each include combination is a separate cache entry
How GraphQL Maps to CDN Caching
GraphQL's default transport — POST requests to /graphql — is inherently uncacheable at the CDN layer. The CDN sees every request as a POST to the same URL and correctly treats them all as cache misses. Making GraphQL cacheable requires deliberate architectural choices: persisted queries, GET-based request encoding, query normalization, or edge function middleware.
Once these mechanisms are in place, GraphQL caching has a structural advantage over REST: the response contains exactly the data the client requested, so no bandwidth is wasted caching or transferring unused fields. A mobile client that requests { product(id: 789) { name, price } } receives a smaller response than the full product object, reducing cache storage and transfer costs.
TTL Strategy Differences
REST APIs set TTLs per resource type. Product listings cache for 5 minutes, category trees for 1 hour, user profiles for 30 seconds. The TTL reflects the resource's change frequency, and each resource controls its own cache lifetime independently.
GraphQL responses often span multiple entity types with different freshness requirements. A query that fetches a product name (changes rarely), its price (changes hourly), and its inventory count (changes every second) cannot have a single TTL that satisfies all three data requirements. The response's effective TTL must be the minimum across all resolved fields—in this case, 1 second because of the inventory data.
// REST: per-resource TTL control
GET /products/789 → max-age: 300 (product data, stable)
GET /products/789/price → max-age: 60 (price, updates hourly)
GET /products/789/stock → max-age: 1 (stock, real-time)
// Each URL cached for its own appropriate duration
// GraphQL: minimum TTL across all fields
POST /graphql
{ product(id: 789) { name, price, stockCount } }
→ max-age: 1 (dictated by stockCount, the most volatile field)
// The entire response caches for 1 second because of one field
// GraphQL: split into separate queries for better TTL
POST /graphql { product(id: 789) { name } } → max-age: 300
POST /graphql { product(id: 789) { price } } → max-age: 60
POST /graphql { product(id: 789) { stockCount } } → max-age: 1
// Defeats the purpose of GraphQL (single request for all data)
This minimum-TTL problem is one of GraphQL's most significant CDN caching challenges. Solving it requires either splitting queries (which defeats GraphQL's single-request advantage), implementing field-level caching at the edge (which requires a GraphQL-aware edge function that caches individual fields and assembles responses from cached fragments), or accepting the lowest common TTL and compensating with efficient cache invalidation.
Cache Invalidation: URL Purge vs Entity Tags
REST cache invalidation is straightforward: when /products/789 changes, purge /products/789 from the CDN. The URL is both the resource identifier and the cache key, so invalidation is a direct mapping. The challenge appears with aggregate endpoints and list views: changing product 789 requires purging not just /products/789 but also /products?category=electronics, /products/featured, and any other URL that might contain this product in its response.
GraphQL invalidation is entity-based. When product 789 changes, every cached query that resolved product 789 must be invalidated—regardless of what other data those queries included. This requires maintaining a reverse index from entity IDs to cache keys. CDN features like Fastly's surrogate keys and Cloudflare's cache tags support this pattern: the origin adds tags to each response (Product:789, Category:electronics), and invalidation purges all entries with the matching tag.
// REST invalidation: URL-based
// Product 789 updated:
PURGE /products/789
PURGE /products?category=electronics // might contain product 789
PURGE /products/featured // might contain product 789
PURGE /products/bestsellers // might contain product 789
// Must know ALL URLs that include this product
// GraphQL invalidation: entity-based with surrogate keys
// Each cached query response tagged with its entities:
// Query A (product page) → Tags: [Product:789, Category:5]
// Query B (product list) → Tags: [Product:789, Product:790, Category:5]
// Query C (user favorites) → Tags: [User:42, Product:789]
// Product 789 updated:
PURGE Tag:Product:789
// → Invalidates queries A, B, and C automatically
// No need to know which queries reference this entity
Hybrid Architecture: REST for Reads, GraphQL for Writes
Many production systems use both API styles, leveraging each one's strengths. REST endpoints serve high-traffic, cacheable reads: product catalogs, content pages, search results. These responses cache efficiently at CDN edge with standard HTTP headers. GraphQL serves complex, personalized queries: user dashboards, admin panels, interactive features that require fetching exactly the data each component needs.
The CDN routes requests based on URL path. Everything under /api/v2/* follows REST caching rules. Requests to /graphql go through the GraphQL caching middleware (persisted queries + edge function normalization). This separation allows each API style to use its optimal caching strategy without compromise.
Performance
| Metric | REST (CDN-cached) | GraphQL (no optimization) | GraphQL (persisted queries + edge) |
|---|---|---|---|
| Cache hit ratio | 75-90% | 0% | 70-85% |
| Avg response size | 4-12 KB (fixed schema) | 0.5-3 KB (exact fields) | 0.5-3 KB (exact fields) |
| Round trips per page | 3-8 requests | 1 request | 1 request |
| P95 latency (cached) | 15-40 ms | N/A (not cached) | 20-50 ms |
| P95 latency (uncached) | 80-200 ms per request | 100-300 ms | 100-300 ms |
| CDN config complexity | None | N/A | Edge function + cache rules |
| Invalidation precision | URL-level | N/A | Entity-level (with tags) |
Decision Guide
Choose REST with CDN caching when your API serves a broad audience with predictable data access patterns, when the response shape is relatively stable and well-defined, when CDN caching efficiency is a primary performance requirement, and when your team prefers minimal CDN-level configuration.
Choose GraphQL with CDN caching when clients have highly varied data requirements (mobile vs web vs partner), when reducing over-fetching provides meaningful bandwidth savings, when you can invest in persisted queries and edge function middleware, and when entity-based cache invalidation provides better precision than URL-based purging for your data model.
Choose the hybrid approach when you have both high-traffic cacheable reads (product catalog, content) and complex personalized queries (dashboards, recommendations). Let each API style do what it does best, and configure the CDN to route and cache each style optimally using its native strengths.