In this blog post, we'll show you how to configure and test a rate limiting rule. Rate limiting is a technique used to control the number of requests that can be made to a given endpoint within a specified time period. This can be useful for preventing denial-of-service attacks (DDoS) and enforcing Terms of Service. In order to get started, you’ll need the following:
Prerequisites
A Fastly account, if you don’t already have an account, grab a free account here
A GitHub account
Create a rate limiting rule
Create a rule like the following screenshot. This rule will rate-limit on the request header “api-key”. When there are 5 or more requests with the same api-key within 1 minute window, then that api-key will be blocked.

Test the Rule with a Github Action
Now that the rule has been deployed, you can test it by making a series of requests to the protected endpoint. In this example, you will use a Github action to spin up a Next-Gen WAF Agent. With the same GitHub Action, you will send requests with curl to the Agent. If you make more requests than the specified limit, you should receive a 429 Too Many Requests error.
For the following GitHub Action to work you will need to set up your Next-Gen WAF Agent Access Key ID and Secret Access Key as Github Secrets.
name: Rate Limiting Demo
on:
workflow_dispatch:
jobs:
ngwaf-rate-limiting-demo:
runs-on: ubuntu-latest
environment: production
timeout-minutes: 2
services:
sigsci:
image: signalsciences/sigsci-agent:latest
env:
SIGSCI_ACCESSKEYID: ${{ secrets.NGWAF_TERRAFORM_NGWAF_SITE_ACCESSKEYID }}
SIGSCI_SECRETACCESSKEY: ${{ secrets.NGWAF_TERRAFORM_NGWAF_SITE_SECRETACCESSKEY }}
SIGSCI_REVPROXY_LISTENER: "app1:{listener=http://0.0.0.0:8888,upstreams=https://http-me.edgecompute.app:443/,pass-host-header=false};"
SIGSCI_UPLOAD_INTERVAL: "5s"
ports:
- 8888:8888
steps:
- name: Wait for NGWAF Agent
run: |
# Wait for agent to be ready
sleep 5
- name: Send requests through NGWAF
run: |
echo "#### Sending requests for rate limiting test domains"
for i in {1..10}; do
echo "Request Number: $i"
response=$(curl -si -H 'host:api.bubbs.coffee' -X POST 'http://0.0.0.0:8888/anything/start-ai-chat' -H api-key:gh-action-rl-demo)
echo "$response"
# Extract the HTTP status code from the first line (e.g., "HTTP/1.1 406 Not Acceptable")
status=$(echo "$response" | head -n 1 | awk '{print $2}')
if [ $i -gt 4 ] && [ "$status" != "406" ]; then
echo "Error: Unexpected status code: $status on request $i"
exit 1
fi
echo
done
- name: Wait for logs to upload
run: |
# Wait for agent to upload logs
sleep 10
Example of failed GitHub Action output
Without the rule in place, you should see the job fail at the 5th request.
#### Sending requests for rate limiting test domains
Request Number: 1
HTTP/1.1 200 OK
Content-Length: 564
Content-Type: application/json
Date: Thu, 06 Feb 2025 22:16:03 GMT
X-Served-By: cache-iad-kiad7000049-IAD
{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"4b89e8de722e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67a534a42c015ef72d509c9f","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT"},"ip":"172.214.46.82","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
Request Number: 2
HTTP/1.1 200 OK
Content-Length: 564
Content-Type: application/json
Date: Thu, 06 Feb 2025 22:16:03 GMT
X-Served-By: cache-iad-kiad7000057-IAD
{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"4b89e8de722e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67a534a42c015ef72d509ca0","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT"},"ip":"172.214.46.82","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
... truncated for brevity
Request Number: 5
HTTP/1.1 200 OK
Content-Length: 564
Content-Type: application/json
Date: Thu, 06 Feb 2025 22:16:03 GMT
X-Served-By: cache-iad-kiad7000153-IAD
{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"4b89e8de722e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67a534a42c015ef72d509ca3","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT"},"ip":"172.214.46.82","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
Error: Unexpected status code: 200 on request 5
Example of successful GitHub Action output
Below is an example that shows a successful GitHub Action run. The block action 406 is returned on requests 5 through 10.
#### Sending requests for rate limiting test domains
Request Number: 1
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:19 GMT
X-Served-By: cache-chi-kigq8000027-CHI
{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e138fd","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
Request Number: 2
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:20 GMT
X-Served-By: cache-chi-kigq8000027-CHI
{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e138fe","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
Request Number: 3
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:20 GMT
X-Served-By: cache-chi-kigq8000027-CHI
{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e138ff","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
Request Number: 4
HTTP/1.1 200 OK
Content-Length: 582
Content-Type: application/json
Date: Tue, 25 Feb 2025 20:59:20 GMT
X-Served-By: cache-chi-kigq8000027-CHI
{"args":"","body":"","headers":{"accept":"*/*","accept-encoding":"gzip","api-key":"gh-action-rl-demo","content-length":"0","host":"http-me.edgecompute.app","user-agent":"curl/8.5.0","x-forwarded-for":"172.18.0.1","x-forwarded-host":"api.bubbs.coffee","x-forwarded-port":"80","x-forwarded-server":"a1f860dbb73e","x-real-ip":"172.18.0.1","x-sigsci-agentresponse":"200","x-sigsci-requestid":"67be2f28d07cd4dde0e13900","x-sigsci-tags":"NO-CONTENT-TYPE,SUSPECTED-BOT,site.api-key-rl"},"ip":"172.183.229.144","method":"POST","url":"https://http-me.edgecompute.app/anything/start-ai-chat"}
Request Number: 5
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 25 Feb 2025 20:59:20 GMT
Content-Length: 20
406 Not Acceptable
... truncated for brevity
Request Number: 10
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 25 Feb 2025 20:59:20 GMT
Content-Length: 20
406 Not Acceptable
Ensure security settings are validated
We've outlined a practical approach to testing rate-limiting rules in Fastly's Next-Gen WAF using GitHub Actions. This method allows for automated validation of these rules, crucial for defending against cyber attacks and enforcing usage policies. Watch the demo for a hands-on example: Github Action and NGWAF Rate Limiting Demo.