Introduction
You have a public website fronted by Amazon CloudFront that serves static files from S3. Customers access these files via direct URLs and must be able to download any file at any time without interference. At the same time, you want to stop malicious actors from crawling your entire bucket.
The Challenge
- Goal: Prevent automated scanning of all URLs while still allowing legitimate customers unlimited downloads of the specific files they need.
- Constraint: No user login or authentication. Files are freely downloadable, so you cannot simply gate them behind a sign-in flow.
Why Plain AWS WAF Rate Limiting Is Not Enough
AWS WAF lets you define rate-limit rules keyed by source IP or by fingerprinting mechanisms such as JA3 and JA4. In theory, you could set:
- A low limit such as 10 requests per minute, which blocks scanners effectively but risks blocking legitimate high-throughput customers.
- A high limit, which lets scanners creep through, especially if attackers distribute requests across IPs or devices.
The result is an uncomfortable trade-off: too low hurts real users, too high fails to stop attackers.
AWS WAF's View of Origin Responses and ATP Rules
By default, custom AWS WAF rules only inspect request attributes. They do not know whether your origin returned 200 OK or 404 Not Found.
The only built-in AWS WAF rules that inspect responses are the Account Takeover Prevention (ATP) managed rules. Those require you to map login fields and are designed for authentication endpoints, not static file downloads.
Why Lambda@Edge Alone Cannot Solve It
Lambda@Edge runs per request and has no built-in shared global state. It cannot maintain counters across all executions, so by itself it cannot enforce a global request threshold.
A Hybrid Approach: WAF + Lambda@Edge
You can combine WAF's global counting capabilities with Lambda@Edge's ability to modify HTTP responses.
1) Primary WAF Rate-Limit Rule (Soft Threshold)
- Type: Rate-based statement, for example 10 requests per 5 minutes
-
Action:
Count(notBlock) -
Custom response header: Insert
X-RateLimit-Exceeded: true
AWS WAF prefixes custom header names with x-amzn-waf-. So if you specify X-RateLimit-Exceeded, downstream components will see:
x-amzn-waf-x-ratelimit-exceeded
2) Secondary WAF Rate-Limit Rule (Hard Threshold)
- Type: Rate-based statement with a much higher threshold, for example 1,000 requests per 5 minutes
-
Action:
Block
This immediately stops heavy-volume attacks at the WAF layer, prevents excessive Lambda@Edge invocations, and reduces cost.
3) Lambda@Edge Function (Origin Response Trigger)
- Trigger: CloudFront Origin Response event
exports.handler = async (event) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
// AWS WAF prefixes headers with x-amzn-waf-
const flag = headers['x-amzn-waf-x-ratelimit-exceeded'];
if (flag && flag[0].value === 'true' && response.status !== '200') {
return {
status: '429',
statusDescription: 'Too Many Requests',
headers: {
'content-type': [{ key: 'Content-Type', value: 'text/html' }]
},
body: '<html><body><h1>Rate Limit Reached</h1><p>Please try again later.</p></body></html>'
};
}
return response;
};
Associate and Deploy
- Attach your Web ACL, containing both rate-limit rules and any ATP group you choose to use, to the CloudFront distribution.
- Deploy the Lambda@Edge function through the CloudFront console or the AWS CLI.
Testing
Legitimate Access
Repeatedly fetch an existing file. The soft-limit counter will increment, but users will still receive the file until the hard threshold is crossed.
Scanning Attempts
Request many non-existent URLs. Errors quickly hit the soft threshold, causing your custom 429 response page. Extreme traffic volumes hit the hard threshold and are blocked at WAF before Lambda@Edge runs.
Demo video
Benefits of This Pattern
-
Precision: Rate limits are tied to actual
Not Foundor error responses. -
User Experience: Legitimate customers getting
200 OKresponses are not blocked unless they truly exceed your thresholds. - Cost Efficiency: High-volume attacks are stopped before Lambda@Edge runs.
- AWS-native design: Uses AWS WAF, CloudFront, and Lambda@Edge without adding external state stores or proxy layers.
References
- AWS WAF Rate-based rules
- AWS WAF Custom HTTP headers
- AWS WAF Custom responses for Block actions
- AWS WAF Fraud Control ATP
Originally published on LinkedIn on May 8, 2025.

Top comments (0)