Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 081 / 100

Rate-based WAF rules: what every AWS developer should know

Stop brute force and scraping at the edge.

Expona Admin8 min read

Rate-based WAF rules

Brute force attacks and malicious scraping can drain your infrastructure, consume database connections, and degrade service for legitimate users. Rate-based WAF rules let you detect and block excessive request patterns at the AWS edge, stopping attackers before they reach your application. By counting requests per IP address over a sliding time window, you gain a lightweight defense that doesn't require application-level changes.

Why rate limiting matters at the WAF layer

I've seen teams implement rate limiting inside their application code—validating each request in middleware, checking Redis counters, returning 429 responses. It works, but it's reactive: you're still consuming compute, database resources, and network bandwidth processing attack traffic. A WAF rate-based rule stops the request before it traverses your ALB, crosses your VPC, or touches your Lambda functions.

The cost savings are real. If you're absorbing 100,000 requests per second from a botnet, each hitting your app layer, you're paying for that egress, that container CPU, that database connection pool slot. Moving that decision to CloudFront or the WAF means you've already said "no" at the edge.

Limit requests per IP over a time window

A rate-based rule in AWS WAF counts HTTP requests from a single IP address within a specified time window (typically 5 minutes) and takes action if the count exceeds your threshold.

Here's how to create one using the AWS CLI:

bash
aws wafv2 create-web-acl \
  --name RateLimitedACL \
  --scope REGIONAL \
  --default-action Allow={} \
  --rules '[
    {
      "Name": "RateLimitRule",
      "Priority": 0,
      "Statement": {
        "RateBasedStatement": {
          "Limit": 2000,
          "AggregateKeyType": "IP"
        }
      },
      "Action": {
        "Block": {}
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "RateLimitRule"
      }
    }
  ]' \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=RateLimitedACL \
  --region us-east-1

This rule allows 2,000 requests per IP over 5 minutes. Any IP exceeding that threshold gets blocked. The AggregateKeyType: IP means counting happens per-source IP, not globally.

In practice, I've found that generic rate limits of 2,000–5,000 requests per IP work well for public APIs and web applications. For internal APIs or when you expect legitimate traffic spikes, you might go higher (10,000+). For login pages under attack, I've tuned it down to 100–300 requests per IP per 5 minutes.

Scope to login and login paths

Applying a blanket rate limit to every path can break legitimate workflows. A search API might legitimately send 50 requests in a few seconds if the client is auto-completing or paginating. A login endpoint, on the other hand, has no good reason to receive 1,000 requests from the same IP in 5 minutes—that's a brute force attempt.

Use a ByteMatchStatement to scope your rate-based rule to specific paths:

bash
aws wafv2 create-web-acl \
  --name LoginRateLimitACL \
  --scope REGIONAL \
  --default-action Allow={} \
  --rules '[
    {
      "Name": "LoginRateLimit",
      "Priority": 0,
      "Statement": {
        "AndStatement": {
          "Statements": [
            {
              "ByteMatchStatement": {
                "SearchString": "/auth/login",
                "FieldToMatch": {
                  "UriPath": {}
                },
                "TextTransformation": "LOWERCASE",
                "PositionalArgument": "EXACTLY"
              }
            },
            {
              "RateBasedStatement": {
                "Limit": 100,
                "AggregateKeyType": "IP"
              }
            }
          ]
        }
      },
      "Action": {
        "Block": {}
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "LoginRateLimit"
      }
    }
  ]' \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=LoginRateLimitACL \
  --region us-east-1

This rule blocks any IP sending more than 100 requests to /auth/login in 5 minutes. Legitimate users logging in once or twice will pass through. Bots hammering credentials won't.

You can apply similar scoping to other high-value targets: search endpoints, API token endpoints, password reset forms, and file upload paths. I typically create separate rules for each path with tuned thresholds.

Use Count mode first, then block

Before you flip the switch to actually block traffic, run your rule in Count mode for a week or two. In Count mode, WAF logs every match but doesn't actually block the request—it just increments a CloudWatch metric.

To create a rule in Count mode:

bash
aws wafv2 create-web-acl \
  --name LoginRateLimitACL \
  --scope REGIONAL \
  --default-action Allow={} \
  --rules '[
    {
      "Name": "LoginRateLimit",
      "Priority": 0,
      "Statement": {
        "AndStatement": {
          "Statements": [
            {
              "ByteMatchStatement": {
                "SearchString": "/auth/login",
                "FieldToMatch": {
                  "UriPath": {}
                },
                "TextTransformation": "LOWERCASE",
                "PositionalArgument": "EXACTLY"
              }
            },
            {
              "RateBasedStatement": {
                "Limit": 100,
                "AggregateKeyType": "IP"
              }
            }
          ]
        }
      },
      "Action": {
        "Count": {}
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "LoginRateLimit"
      }
    }
  ]' \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=LoginRateLimitACL \
  --region us-east-1

During this window, check your CloudWatch dashboard for the LoginRateLimit metric. Are there legitimate users hitting the threshold? Are scrapers or bots being caught? You'll get a feel for whether 100 requests per 5 minutes is the right threshold or if you need to adjust.

Once you're confident—usually after 7–14 days of monitoring—switch the action to Block: {} and deploy. I've made the mistake of jumping straight to blocking without observing first, and it resulted in locking out a batch operation that one customer ran daily. Count mode saved me from that mistake.

Combine with bot control rules

Rate limiting works well for volume-based attacks, but sophisticated bots can stay under your rate limit by spreading requests across time or multiple IPs. AWS WAF Bot Control uses machine learning to identify bot traffic patterns—user agents, TLS fingerprints, behavioral signals—and can complement your rate-based rules.

Here's how to layer them together:

bash
aws wafv2 create-web-acl \
  --name LayeredProtectionACL \
  --scope REGIONAL \
  --default-action Allow={} \
  --rules '[
    {
      "Name": "AWSManagedRulesBotControl",
      "Priority": 0,
      "OverrideAction": {
        "None": {}
      },
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesBotControlRuleGroup"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "BotControlRule"
      }
    },
    {
      "Name": "LoginRateLimit",
      "Priority": 1,
      "Statement": {
        "AndStatement": {
          "Statements": [
            {
              "ByteMatchStatement": {
                "SearchString": "/auth/login",
                "FieldToMatch": {
                  "UriPath": {}
                },
                "TextTransformation": "LOWERCASE",
                "PositionalArgument": "EXACTLY"
              }
            },
            {
              "RateBasedStatement": {
                "Limit": 100,
                "AggregateKeyType": "IP"
              }
            }
          ]
        }
      },
      "Action": {
        "Block": {}
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "LoginRateLimit"
      }
    }
  ]' \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=LayeredProtectionACL \
  --region us-east-1

Now your ACL has two layers: AWS Bot Control catches obvious bots and scrapers before they even reach your rate-limit rule, and the rate-limit rule catches low-and-slow attacks or legitimate tools that bypass bot signals.

Bot Control does add cost (around $10 per million requests), but it's worth it if you're seeing sophisticated scraping or credential stuffing. I've had it identify bots that made only 5–10 requests per minute—far below my rate limit—but with suspicious TLS fingerprints or user agents. The combination is stronger than either rule alone.

Monitoring and tuning

Once your rule is live, set up CloudWatch alarms to alert when blocked request counts spike. A sudden jump might indicate a new attack vector or a misconfiguration.

Use the sampled requests feature in the AWS WAF console to inspect what's being blocked. You'll see the IP, the User-Agent, the URI, and the time of the request. This data helps you refine thresholds and identify false positives.

I typically review WAF logs weekly for the first month, then monthly after that. If you're using CloudFront or ALB, enable full logging to S3 for deeper analysis:

bash
aws wafv2 put-logging-configuration \
  --logging-configuration \
    ResourceArn=arn:aws:wafv2:us-east-1:123456789012:regional/web-acl/LoginRateLimitACL/12345678-1234-1234-1234-123456789012,\
    LogDestinationConfigs=arn:aws:logs:us-east-1:123456789012:log-group:/aws/waf/login-rate-limit \
  --region us-east-1

Then query those logs with Athena or CloudWatch Insights to find patterns.

Common mistakes

  • Setting the threshold too low: If you limit legitimate users to 50 requests per 5 minutes on a search endpoint, they'll get blocked during legitimate heavy usage. Start high, observe, then gradually tighten.
  • Not scoping to specific paths: A global rate limit of 100 requests per IP will break any endpoint with legitimate bulk operations. Always use AndStatement to target high-risk paths.
  • Jumping straight to blocking: Count mode exists for a reason. Run it for at least a week. I've seen teams deploy rate limits during off-hours and discover massive false positives the next morning.
  • Forgetting about IPv6: If your application accepts IPv6, you need a separate rate-based rule for AggregateKeyType: FORWARDED_IP if you're behind a proxy, or adjust for IPv6 CIDR ranges.
  • Ignoring CloudWatch metrics: If you're not actively monitoring the WAF dashboard, you won't notice when a threshold needs adjustment or when attack patterns change.

Key takeaways

  • Rate-based WAF rules count requests per IP over a 5-minute window and block when a threshold is exceeded, stopping brute force and scraping attacks at the edge.
  • Use ByteMatchStatement to scope rules to high-risk paths like /auth/login, /api/search, or /password-reset rather than applying a global limit.
  • Always run new rules in Count mode for 1–2 weeks to observe real traffic patterns and avoid false positives before switching to Block.
  • Combine rate-based rules with AWS Bot Control for defense-in-depth: Bot Control catches obvious bots, rate limits catch volume-based attacks and low-and-slow patterns.
  • Monitor CloudWatch metrics and sampled requests weekly in the first month, then monthly thereafter, to tune thresholds and identify new attack vectors.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles