Skip to content

Response Caching

Effect GQL supports automatic HTTP Cache-Control header generation based on field-level cache hints. This enables CDN caching for public queries and browser caching for user-specific data.

Enable cache control by adding cacheControl hints to your fields:

import { GraphQLSchemaBuilder, query, objectType, field, toRouter } from "@effect-gql/core"
import { serve } from "@effect-gql/node"
import { Effect, Layer } from "effect"
import * as S from "effect/Schema"
const UserSchema = S.Struct({
id: S.String,
name: S.String,
email: S.String,
})
const builder = GraphQLSchemaBuilder.empty.pipe(
// Type-level hint: all User fields cache for 1 hour
objectType({
name: "User",
schema: UserSchema,
cacheControl: { maxAge: 3600 },
}),
// Field-level hint: email is private, cache for 1 minute
field("User", "email", {
type: S.String,
cacheControl: { maxAge: 60, scope: "PRIVATE" },
resolve: (user) => Effect.succeed(user.email),
}),
// Query with cache hint
query("users", {
type: S.Array(UserSchema),
cacheControl: { maxAge: 300 }, // 5 minutes
resolve: () => Effect.succeed([
{ id: "1", name: "Alice", email: "alice@example.com" },
]),
}),
)
const router = toRouter(builder, Layer.empty, {
cacheControl: { enabled: true },
})
serve(router, Layer.empty, { port: 4000 })

Effect GQL computes the overall cache policy by analyzing all fields in the query:

  1. maxAge: Uses the minimum maxAge of all resolved fields
  2. scope: Set to PRIVATE if any field is PRIVATE
# Query both public and private data
query {
users { # maxAge: 300, PUBLIC
id # inherits from User type: 3600
name # inherits from User type: 3600
email # maxAge: 60, PRIVATE (field override)
}
}

Result: Cache-Control: private, max-age=60

The response uses maxAge: 60 (minimum) and scope: PRIVATE (any field is private).

Based on the computed policy, Effect GQL sets the Cache-Control header:

PolicyHeader
maxAge: 0Cache-Control: no-store
maxAge: 3600, scope: PUBLICCache-Control: public, max-age=3600
maxAge: 60, scope: PRIVATECache-Control: private, max-age=60

Apply cache hints to individual query or object fields:

query("publicData", {
type: S.String,
cacheControl: { maxAge: 3600 }, // Cache for 1 hour
resolve: () => Effect.succeed("data"),
})
field("User", "recommendations", {
type: S.Array(ProductSchema),
cacheControl: { maxAge: 300, scope: "PRIVATE" }, // User-specific
resolve: (user) => getRecommendations(user.id),
})

Apply default cache hints to all fields returning a type:

objectType({
name: "Product",
schema: ProductSchema,
cacheControl: { maxAge: 3600 }, // All Product fields cache 1 hour
})

Field-level hints override type-level hints.

interface CacheHint {
// Maximum age in seconds (0 = no cache)
maxAge?: number
// PUBLIC: CDN + browser cacheable
// PRIVATE: Browser only (user-specific)
scope?: "PUBLIC" | "PRIVATE"
// Inherit maxAge from parent field
inheritMaxAge?: boolean
}

Effect GQL follows these defaults (matching Apollo Server):

Field TypeDefault maxAgeBehavior
Root fields (Query)0Must opt-in to caching
Object-returning fields0Must opt-in to caching
Scalar fieldsinheritedInherits parent’s maxAge
Introspection fieldsignoredDon’t affect cache policy
const router = toRouter(builder, layer, {
cacheControl: {
// Enable cache control (default: true when cacheControl is set)
enabled: true,
// Default maxAge for fields without hints (default: 0)
defaultMaxAge: 0,
// Default scope for fields without hints (default: "PUBLIC")
defaultScope: "PUBLIC",
// Set HTTP headers on responses (default: true)
calculateHttpHeaders: true,
},
})
Terminal window
GRAPHQL_CACHE_CONTROL_ENABLED=true
GRAPHQL_CACHE_CONTROL_DEFAULT_MAX_AGE=0
GRAPHQL_CACHE_CONTROL_DEFAULT_SCOPE=PUBLIC

Cache control works best with Persisted Queries and GET requests:

import { makePersistedQueriesRouter } from "@effect-gql/persisted-queries"
const router = makePersistedQueriesRouter(schema, layer, {
mode: "apq",
enableGet: true, // Enable GET requests for CDN caching
cacheControl: { enabled: true },
})

When a client sends a GET request with a persisted query hash, CDNs can cache the response based on the URL:

GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"abc123"}}
Cache-Control: public, max-age=3600
// Cloudflare respects Cache-Control headers by default
// Configure cache rules in dashboard or via Page Rules
import { computeCachePolicyFromQuery, toCacheControlHeader } from "@effect-gql/core/server"
const policy = await Effect.runPromise(
computeCachePolicyFromQuery(
"{ users { id name } }",
undefined, // operationName
schema,
builder.getCacheHints(),
{ defaultMaxAge: 0 }
)
)
console.log(policy) // { maxAge: 300, scope: "PUBLIC" }
console.log(toCacheControlHeader(policy)) // "public, max-age=300"
const hints = builder.getCacheHints()
// Map<string, CacheHint>
// "Query.users" -> { maxAge: 300 }
// "User" -> { maxAge: 3600 }
// "User.email" -> { maxAge: 60, scope: "PRIVATE" }

Default to maxAge: 0 and explicitly opt-in to caching:

// Only cache fields you've verified are safe
query("publicPosts", {
type: S.Array(PostSchema),
cacheControl: { maxAge: 300 }, // Explicitly enable
resolve: () => getPosts(),
})
query("me", {
type: UserSchema,
cacheControl: { maxAge: 60, scope: "PRIVATE" },
resolve: () => getCurrentUser(),
})
field("User", "settings", {
type: SettingsSchema,
cacheControl: { scope: "PRIVATE" }, // Even with inheritance
resolve: (user) => getSettings(user.id),
})
// All Product data is public and stable
objectType({
name: "Product",
schema: ProductSchema,
cacheControl: { maxAge: 3600 },
})
// Override only for dynamic fields
field("Product", "inventory", {
type: S.Number,
cacheControl: { maxAge: 60 }, // More volatile
resolve: (product) => getInventory(product.id),
})
const router = toRouter(builder, layer, {
complexity: { maxComplexity: 1000 },
cacheControl: { enabled: true },
})
ExportDescription
CacheHintCache hint with maxAge, scope, inheritMaxAge
CacheControlScope"PUBLIC" or "PRIVATE"
CachePolicyComputed policy with maxAge and scope
CacheHintMapMap of type/field names to cache hints
CacheControlConfigRouter configuration options
ExportDescription
computeCachePolicy(info)Compute cache policy from parsed query
computeCachePolicyFromQuery(query, ...)Compute cache policy from query string
toCacheControlHeader(policy)Convert policy to HTTP header value
ExportDescription
CacheControlConfigFromEnvEffect Config for environment variables