How do you implement GraphQL caching and pagination effectively?

Approaches for predictable, efficient GraphQL caching and pagination without overfetching or surprises.
Learn to design GraphQL caching and pagination using normalized caches, cache hints, persisted queries, and cursor-based patterns that stay predictable and fast.

answer

I make GraphQL queries predictable by using persisted allowlisted operations, depth/complexity limits, and stable cursor-based pagination with deterministic sort keys. For caching, I layer a normalized client cache (Apollo/Relay) over server and edge caches: response and entity caches with Cache-Control hints, short TTLs, and tag or entity-key invalidation. I eliminate N+1 with DataLoader, use Automatic Persisted Queries over GET for CDN hits, and apply SWR for safe staleness on read-heavy data.

Long Answer

Designing caching and pagination in GraphQL is about predictability: clients should know what they will get, and servers should know what they will pay. I use layered caching, stable cursors, disciplined schema design, and operational guardrails.

1) Make queries predictable before you cache
Caching brittle, unpredictable queries fails quickly. I:

  • Persist and allowlist queries (APQ or full persisted operations). Each document has a stable hash, ideal for GET + CDN caching.
  • Enforce depth, breadth, and cost limits with a query complexity analyzer and timeouts to protect upstreams.
  • Keep resolvers pure and idempotent for cacheability; side effects are not allowed in read paths.
  • Ensure deterministic ordering wherever pagination exists, so cursors remain stable.

2) Layered caching model
I separate caches by responsibility and invalidation scope:

  • Client-side normalized cache (Apollo/Relay): Entities keyed by __typename:id. Field policies (keyArgs, merge) handle pagination windows and entity merging. Relay’s store and Apollo’s type policies prevent duplication and overfetch.
  • Server-side caches:
    • Response cache: keyed by (operation hash, variables, user identity where relevant). Short TTL or SWR for read-most queries.
    • Entity/object cache: cache data-loader outputs per key (for example Redis). This maximizes N+1 elimination across concurrent resolvers.
    • Field-level caching: for expensive computed fields (rate cards, exchange rates) with brief TTLs.
  • Edge/CDN caching: Use GET with APQ; include Cache-Control hints from the schema to set TTL, stale-while-revalidate, and Vary on auth headers. For public data, allow long TTL; for user-scoped data, skip or shard by user token hash.

3) Cache hints and directives
Adopt schema-driven cache metadata:

  • Use @cacheControl (or similar) with maxAge, scope: PUBLIC|PRIVATE, and optional tags.
  • Map hints to HTTP headers at the gateway. For private data, emit Cache-Control: private, max-age=0 but still allow entity cache deeper in the stack.
  • Prefer tag-based invalidation where supported (for example products:123) to surgically purge downstream caches.

4) Invalidation strategy
Invalidation is the hard part; I keep it explicit and event-driven:

  • On mutation, identify touched entity keys and publish invalidation events (Redis pub/sub, Kafka). Consumers evict response cache entries whose dependency set includes those keys.
  • Use versioned entity records (increment a version in Redis) so cached responses that embed old versions are naturally bypassed.
  • Favor SWR for read-heavy domains: serve cached data instantly, trigger background revalidation, then notify clients via subscriptions or refetch policies.

5) Pagination that does not surprise
Offset pagination is easy but fragile under writes; I generally choose cursor-based pagination with Relay-style connections:

  • Connections: edges { node, cursor } and pageInfo { hasNextPage, endCursor }.
  • Stable cursors: opaque, derived from a deterministic sort key (for example createdAt,id). Never encode raw offsets that shift under inserts.
  • Windows: limit page size with a server cap; reject unbounded requests.
  • Bidirectional: support first/after and last/before only when the sort is fully deterministic.
  • Keyset queries: use WHERE (created_at, id) > (:cursorCreatedAt, :cursorId) to avoid OFFSET scans.

6) Efficiency in resolvers

  • Use DataLoader (or equivalent) for per-request batching and memoization. This collapses N+1 into O(1) batched fetches, feeding the entity cache.
  • Apply projection (select only requested fields) and lookahead where possible to avoid round trips and heavy payloads.
  • Keep resolvers small, functional, and cache-aware; prefer returning ids that the client cache can normalize.

7) Transport choices for caching

  • For cacheable queries: GET + APQ so a CDN can key on the SHA and variables.
  • For private or mutation-heavy paths: POST and rely on server/entity caches only.
  • Compress responses (Brotli), and set ETag to the operation hash plus entity versions to support revalidation.

8) Observability and guardrails

  • Track cache hit rates by layer (client, response, entity, CDN), average render time, and NX misses.
  • Emit pagination metrics: average page size, hasNextPage distribution, and backfill latency.
  • Automated tests enforce that connection fields are sorted deterministically and that pageInfo behaves under simulated inserts.

9) Special cases

  • Subscriptions: not cacheable, but can drive cache updates on the client.
  • Authenticated feeds: cache privately at the gateway with short TTL and strict Vary.
  • Highly volatile lists: prefer smaller windows with high-frequency revalidation over large pages that stale quickly.

The playbook: persist queries, normalize entities, cache at multiple layers with explicit hints, and choose cursor pagination with deterministic keys. Measure, invalidate precisely, and treat caching as a first-class contract between schema and runtime.

Table

Area Strategy Tools / Patterns Outcome
Predictability Persisted allowlisted queries, cost limits APQ, depth/complexity analyzers Stable, cacheable requests
Client Cache Normalize by __typename:id, field policies Apollo type policies, Relay store Fewer round trips, deduped data
Server Cache Response + entity caches, SWR Redis, in-memory LRU, cache hints Fast reads, controlled staleness
Edge Cache GET + APQ, Cache-Control, ETag CDN, gateway headers Public data served at edge
Invalidation Entity tags, mutation-driven purge Pub/sub, versioning, tags Precise, low-blast-radius clears
Pagination Cursor-based keyset, Relay connections edges, pageInfo, stable sort No duplicates, no skips under writes

Common Mistakes

  • Using offset pagination on volatile feeds, causing duplicates or missing items.
  • No persisted queries, so CDNs cannot cache and schemas see unbounded variance.
  • Caching private responses at the edge without Vary on auth, leading to leaks.
  • Overreliance on response cache instead of fixing N+1 with DataLoader.
  • Non-deterministic sorts that break cursor stability.
  • Missing invalidation paths after mutations; stale results linger.
  • Letting client caches normalize without stable ids (missing id or __typename).
  • Huge page sizes that defeat predictability and stress upstreams.

Sample Answers

Junior:
“I use Relay-style cursor pagination with edges and pageInfo so inserts do not break order. On the client I let Apollo normalize entities by __typename:id. I turn on APQ so queries can be cached.”

Mid-level:
“I persist and allowlist queries, apply depth and cost limits, and serve cacheable reads over GET for CDN hits. On the server I combine a short-TTL response cache with a Redis entity cache fed by DataLoader. Pagination is keyset-based with opaque cursors from (createdAt,id).”

Senior:
“I design cache hints in the schema, emit Cache-Control from the gateway, and use tag-based invalidation tied to mutations. Query transport uses APQ + GET for public reads, POST for private. Entity caches collapse N+1; client caches reconcile via normalized ids. Pagination uses deterministic keyset cursors with strict page caps and tests to prevent drift.”

Evaluation Criteria

Strong answers demonstrate:

  • Persisted queries and cost controls for predictability.
  • Layered caching (client normalization, server response/entity, edge with headers).
  • Deterministic cursor-based pagination with keyset queries.
  • Invalidation discipline (entity tags, mutation-driven purge).
  • Resolver efficiency (DataLoader, projection) and observability (hit rates, latency).
    Red flags: only “use Apollo cache,” offset pagination on changing lists, no plan for invalidation, or caching authenticated responses at the edge without scoping.

Preparation Tips

  • Implement Relay connections and keyset queries against a real dataset with inserts.
  • Configure Apollo type policies (keyFields, merge) and test pagination merges.
  • Add APQ to a gateway, serve queries over GET, and verify CDN caching with headers.
  • Build a small response cache and a Redis DataLoader cache; measure hit rates.
  • Simulate mutation-driven invalidation via pub/sub and tags.
  • Add query cost analysis and depth limits; run load tests to confirm protections.
  • Write unit tests asserting stable sort and pageInfo correctness under concurrent writes.

Real-world Context

A marketplace moved from offset to cursor pagination over (created_at,id), eliminating duplicate and missing items as listings surged. Persisted GET queries with APQ enabled edge caching for public catalog pages, dropping P95 by 45%. Behind the gateway, a Redis entity cache fed by DataLoader cut database queries by 70% during peaks. Mutations published entity tags to purge response cache entries selectively, avoiding global invalidations. Observability dashboards monitored cache hit rates, page window sizes, and cursor drift, keeping pagination predictable and fast.

Key Takeaways

  • Persist and allowlist queries; enforce depth and cost limits.
  • Normalize client data and cache entities server-side to kill N+1.
  • Use schema cache hints and headers; prefer GET + APQ for edge caching.
  • Choose cursor-based, keyset pagination with deterministic sorts.
  • Invalidate by entity tags on mutation; measure hit rates and tail latency.

Practice Exercise

Scenario:
You are building a GraphQL feed for articles with comments that updates frequently and serves both anonymous and logged-in users.

Tasks:

  1. Define a Relay-style articlesConnection with stable ordering (publishedAt,id) and opaque cursors. Implement keyset queries for first/after.
  2. Add @cacheControl hints to Article, Author, and Comment with appropriate maxAge and scope. Emit headers from the gateway.
  3. Enable APQ; serve public article list queries over GET with CDN caching. For authenticated dashboards, keep POST and bypass edge caches.
  4. Implement DataLoader for authors and comment counts; add a Redis entity cache.
  5. Build a short-TTL response cache keyed by (opHash, variables, userScope); attach entity tags to entries.
  6. On createComment and updateArticle, publish invalidation events by entity tag; evict only affected responses.
  7. Add analytics for cache hit rates, P95 latency, and pagination integrity (duplicate or skipped ids). Write tests for pageInfo under concurrent inserts.

Deliverable:
A design and validation report showing predictable cursor pagination, layered caching with precise invalidation, and measured gains in latency and database load.

Still got questions?

Privacy Preferences

Essential cookies
Required
Marketing cookies
Personalization cookies
Analytics cookies
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.