Caching in Amazon API Gateway looks like an easy thing to configure while the outcome is huge: responses get faster, the bill gets smaller, TTL is configured and you didn’t touch a single line of application code.
There’s a catch, though, and it’s easy to miss because nothing breaks. No errors, no failed deployments, no alarms. Your API just starts returning the wrong data to some users, and it looks completely normal while doing it.
This post covers how API Gateway decides what to cache, why query parameters fall through the cracks, and how to set it up properly
How API Gateway caching works
Caching is available on REST APIs (not HTTP APIs) and is enabled per stage. When you turn it on, API Gateway provisions a dedicated cache cluster for that stage. Sizes range from 0.5 GB up to 237 GB.
The flow looks like this:
- A request comes in.
- API Gateway builds a cache key from the request.
- If there’s a fresh entry for that key, it returns it straight away. Your backend is never called.
- If not, it calls your backend, returns the response, and stores it under that key for the TTL (300 seconds by default, up to 3600).
This is rather simple but everything depends on step 2 and what goes into that cache key.
What is a cache key?
A cache key is how API Gateway decides whether two requests are “the same request”. If two requests produce the same key, they get the same cached response.
And here’s the important part: by default, the cache key is based only on the resource path.
/products?page=1 → /products
/products?page=2 → /products
/products?lang=de → /productsThree different requests, one label, so everyone gets the same box. Query strings are left out, and so are headers like Authorization or Accept-Language. API Gateway doesn’t look at them unless you tell it to.
The trap
Let’s say you have a paginated endpoint, one that returns a long list in smaller chunks (pages), where the client asks for a specific page with a query parameter:
GET /products?page=1
GET /products?page=2To you, these are obviously different requests. To the API Gateway cache, with default settings, both are just /products.
Here’s what happens:
- A user requests
/products?page=1. Cache miss, your Lambda is triggered and returns page 1, and API Gateway caches it under/products. - Another user requests
/products?page=2. API Gateway builds the key/products, finds a fresh entry, and returns page 1. - For the next five minutes, every page of your product list is page 1.
The same goes for any parameter that changes the response:
?category=shoesand?category=booksreturn the same list?from=2026-01-01&to=2026-03-31returns whichever date range was requested first?lang=dereturns English because someone asked for English first
Why this one is problem
Most AWS misconfigurations cause obvious failures. A missing IAM permission gives you a 403. A bad integration gives you a 500. So when you notice, you fix it.
However this one fails silently:
- Every response is a 200. Your monitoring is happy.
- Whatever request comes first after the cache expires “wins”, so the bug seems to come and go.
- It might not show up in testing. Locally and in most dev setups, caching is off. Even with caching on, a quick manual test often hits a cache miss and looks fine.
- It looks like an application bug. Teams can spend hours debugging pagination logic in a Lambda that’s working perfectly, because the Lambda is never even called.
There’s an even worse version. If your endpoint returns user-specific data and Authorization isn’t part of the cache key, one user can get another user’s cached response. Authorization still runs, so only authenticated users get through, but the cached body is shared between all of them. At that point it isn’t a performance bug anymore, it’s a data leak.
The fix: cacheKeyParameters
You tell API Gateway which request parameters belong in the cache key. There are two rules:
- The parameter must be declared on the method (as a query string, header or path parameter).
- It must be listed as a cache key parameter on the integration.
If you define your API with OpenAPI, it looks like this:
/products:
get:
parameters:
- { name: page, in: query }
- { name: category, in: query }
- { name: cache_bypass, in: query }
x-amazon-apigateway-integration:
type: aws_proxy
httpMethod: POST
uri: ${lambda_invoke_arn}
cacheNamespace: products
cacheKeyParameters:
- method.request.querystring.page
- method.request.querystring.category
- method.request.querystring.cache_bypassNow /products?page=1 and /products?page=2 produce different keys and get cached separately.
On the Terraform side, caching is enabled at the stage and method level:
resource "aws_api_gateway_stage" "this" {
rest_api_id = aws_api_gateway_rest_api.this.id
deployment_id = aws_api_gateway_deployment.this.id
stage_name = "prod"
cache_cluster_enabled = true
cache_cluster_size = "0.5"
}
resource "aws_api_gateway_method_settings" "this" {
rest_api_id = aws_api_gateway_rest_api.this.id
stage_name = aws_api_gateway_stage.this.stage_name
method_path = "*/*"
settings {
caching_enabled = true
cache_ttl_in_seconds = 300
cache_data_encrypted = true
}
}If a parameter can change the response, it belongs in the cache key. If it can’t (a tracking ID, a client version used only for logging), leave it out. Otherwise you end up with lots of duplicate entries for the same data.
For user-specific endpoints, add the header that identifies the user (usually method.request.header.Authorization), or don’t cache those endpoints at all.
Getting fresh data on demand
Sooner or later you’ll want to skip the cache. Someone fixes a record in the database and wants to see the change right away, not when the cache expires five minutes later. There are three ways to do it.
1. Change the key with a cache_bypass parameter
Since cache_bypass is part of the cache key, sending a new value makes API Gateway think it’s a brand-new request. It doesn’t find a cached entry, so it calls your backend:
GET /products?page=1&cache_bypass=1728741234Any client can do this since it’s easy. But every new value leaves an extra entry in the cache that nobody will read again. And anyone who finds out about the parameter can skip your cache and hit your backend directly.
2. Ask API Gateway to refresh one entry
If a client sends the Cache-Control: max-age=0 header, API Gateway drops the cached response for that request and fetches a fresh one. Only allow this for authorized callers (the execute-api:InvalidateCache permission). Otherwise anyone can clear your cache entries.
3. Clear everything at once
After a deploy or a big data fix, you can empty the whole cache for a stage:
aws apigateway flush-stage-cache --rest-api-id abc123 --stage-name prodThe parameter is the quickest, the header gives you control over who can refresh, and the flush is for when you want to start clean.
How to check it’s actually working
Verify the following things:
- CloudWatch metrics:
CacheHitCountandCacheMissCountare published per stage. If hits are high on an endpoint where every request should be different, your cache key is probably too broad. - A quick manual test: request
?page=1, then?page=2, and compare the responses. Without cache keys, you’ll see the same body twice. - Backend logs: if your Lambda logs which parameters it received, a second request that never shows up there was answered from the cache.
Conclusion
API Gateway caching is a great feature, but it’s not magic. It doesn’t understand your API. It only knows what you tell it, and by default you’re telling it that the path is the only thing that matters. The fix takes a few lines of configuration. The hard part is knowing you need it before your users notice they keep seeing the same page. A cache is only as smart as its key.



