By Rajesh Kannan, Co-Founder & CTO, Gramosoft · Last updated: · Lab results from our own local test on November 3, 2026 (Python 3.13, requests 2.34, httpx 0.28)

Your scraper's log says every request succeeded, yet a third of the stored pages are a near-empty HTML shell that loads challenge.js. That's AWS WAF, and the reason is a single status code: 202. This guide shows how to recognise AWS WAF, what each of its responses means, how to stop storing challenge pages, and which routes hold up, including GcrawlAI.

TL;DR

  • Recognise it: a 202 or 405 with an x-amzn-waf-action header, a page loading challenge.js from token.awswaf.com, or an aws-waf-token cookie.
  • The 202 trap: requests' r.ok and httpx's is_success treat a challenge as success. In our lab, each kept all 10 challenge pages.
  • Read the header, then the fields: checking x-amzn-waf-action and the items you need kept only the 20 real pages.
  • Tokens expire: the aws-waf-token lasts for the site's immunity time, 300 seconds by default, then the visitor is challenged again.
  • Pick a route you're allowed to use: an official API or permission first; for public pages you may collect, GcrawlAI renders them in a real browser.

How do you scrape an AWS WAF-protected website? First confirm it's AWS WAF from the x-amzn-waf-action header, a challenge.js script on token.awswaf.com, or an aws-waf-token cookie. Plain HTTP clients can't run that script, and they report its 202 as success. Use an official API or ask for access where you can; for public pages you're allowed to collect, fetch them through a real browser, for example with GcrawlAI, and keep a page only if your fields are in it.

Three routes to the same data

Route Best when Trade-off
Official API, feed or data licence The site offers one, or will on request Paperwork, sometimes a fee, but no challenges
Managed browser API Public pages you're allowed to collect, at volume A cost per page, and you still validate fields
Your own browser automation Small, occasional jobs on sites that allow it You own the browsers, IPs, failures and upkeep

Protected pages, one request each: GcrawlAI

AWS WAF's Challenge action serves the page only to a client that runs challenge.js in a real browser, from a reasonable IP, within the site's rate limits. Running that yourself means browsers, proxies and retry logic to maintain. GcrawlAI does it inside one API request and returns the rendered page, so your Python code only checks the fields.

GcrawlAI homepage with the logo and menu (Products, Playground, Extractors, Docs, Pricing, Blog, Use Cases, Custom Request): Transform any website into structured LLM-ready data, the open-source web scraping API for AI agents, RAG pipelines and data teams, with JavaScript rendering, anti-bot stealth and proxies included in every request and no credit system; General, Extractors and Offline Bundle modes, tabs for Scrape, Map, Crawl, Batch, Screenshot, Search and Smart Answer, and the Batch tab open with a URL list, Markdown format, country picker and a Start batch scraping button
The GcrawlAI homepage doubles as a playground: here the Batch tab queues a list of URLs as one job, with rendering, anti-bot stealth and proxies included in every request (desktop screenshot taken October 31, 2026; the phone view, from October 28, shows on small screens).

The homepage says it plainly: JavaScript rendering, anti-bot stealth and proxies are included in every request, with no credit system. On AWS WAF sites, where a page often needs a real browser and a residential IP, that means no multiplier on the requests that matter most.

Split-flap departure board, one protected page, one request with GcrawlAI: 01 your request, one API call with URL and country, sent; 02 real browser runs the page's JavaScript and keeps cookies, on time; 03 residential IP in the country you chose, on time; 04 retries with stealth escalation inside the same request, on time; 05 HTML and Markdown arrive and your code checks the page; succeeded counts as 1 request, failed costs $0
One protected page, one request: the browser, IP and retries travel together.

Here's the client. It asks GcrawlAI for the page and keeps it only if it isn't an AWS WAF challenge or CAPTCHA page and holds the items you need:

# pip install gcrawl-sdk parsel
"""Fetch a public page you're allowed to collect through GcrawlAI, and keep it only if it's
the real page: not an AWS WAF challenge or CAPTCHA page, and holding the items you need."""
import os
import time

from gcrawl_sdk import GcrawlClient, ScrapeOptions
from gcrawl_sdk.exceptions import GcrawlAPIError, GcrawlTimeoutError
from parsel import Selector

gc = GcrawlClient(api_key=os.getenv("GCRAWL_API_KEY", "gc-YOUR_API_KEY"))
OPTS = ScrapeOptions(html_enabled=True, html_clean=False, markdown_enabled=True, proxy_geo="US", timeout=60)
WAF_MARKERS = ("token.awswaf.com", "challenge-container", "captcha-container")


def is_real_page(html: str, item_css: str, min_items: int) -> bool:
    if any(m in html for m in WAF_MARKERS):
        return False
    return len(Selector(html).css(item_css)) >= min_items


def fetch(url: str, item_css: str, min_items: int = 5, tries: int = 3):
    for attempt in range(1, tries + 1):
        try:
            page = gc.scrape(url=url, options=OPTS, wait=True)
            if is_real_page(page.html or "", item_css, min_items):
                return page
            print(f"attempt {attempt}: WAF page or missing items")
        except (GcrawlAPIError, GcrawlTimeoutError) as e:
            print(f"attempt {attempt}: {e}")
        time.sleep(2 ** attempt)                     # back off between tries
    return None


if __name__ == "__main__":
    page = fetch("https://example.com/outdoor/tents", item_css="li.product")
    print(page.markdown[:500] if page else "no usable page")
  • Rendering and access: a real browser through residential IPs in the country you choose, with retries and stealth escalation inside the request.
  • Output you can check: HTML, Markdown or JSON, plus extractors that return structured fields for common page types.
  • Scrape, crawl, screenshots and Search API: scrape single pages, crawl whole sites, capture screenshots, or use the Search API.
  • For developers and AI agents: a Python SDK, an MCP server for Claude, Cursor and other agents, the Playground to test a page before you write code, and the docs.

The MIT-licensed core also runs on your own servers: clone it from GitHub, install the Python requirements, and run the API with Redis and a Celery worker. The setup script is in the code download.

Pricing is a free plan of 50 requests a month with no card, paid plans from $0.67 to $1.45 per 1,000 requests, and a self-hosted core that costs nothing. Failed requests are never charged. The full plan table is in the cost section.

Where it falls short: AWS WAF rules are set per site, so no service clears every deployment every time, GcrawlAI included. Test your own targets on the free plan. It won't solve CAPTCHAs meant for humans or log in for you, and it doesn't make collecting a site's data acceptable when that site's terms say no.

"The AWS WAF bug we see most isn't a block, it's a 202. The client says success, the pipeline stores a challenge page, and nobody notices until the data is wrong. Reading one header fixes it."

Rajesh Kannan, Co-Founder & CTO at Gramosoft

What AWS WAF is

AWS WAF is Amazon's web application firewall. It usually sits in front of a CloudFront distribution, an Application Load Balancer or API Gateway, so every request is inspected at the edge before the site sees it. Site owners attach a web ACL, an ordered list of rules, and the first matching rule decides what happens.

Rules can match IP addresses, countries, headers, paths, cookies and request rates. ZenRows notes that rate limits can be counted per IP, cookie, header or a custom key. On top of the rules, Bot Control runs a JavaScript check in the browser and issues an encrypted token when it passes.

Because every site writes its own rules, two AWS WAF sites can behave very differently. One might only rate-limit a search path; another might challenge every visitor. That's why the response, not the vendor name, tells you what you're dealing with. The AWS WAF product page covers the service itself.

The five actions, as your scraper sees them

Every AWS WAF rule ends in one of five actions, and each looks different from the outside. Two of them, Challenge and CAPTCHA, carry an x-amzn-waf-action header that names the action. That header is the most reliable signal you have, because the status codes alone can mislead your HTTP client.

Sheet of postage stamps, the five AWS WAF actions as a scraper sees them: Allow, HTTP 200, passed to the site; Count, HTTP 200, logged then passed on; Block, HTTP 403 Forbidden by default; Challenge, HTTP 202 with x-amzn-waf-action: challenge, a silent JavaScript check that sets the aws-waf-token cookie; CAPTCHA, HTTP 405 with x-amzn-waf-action: captcha, a puzzle also sent when the token's immunity time runs out
The five AWS WAF actions; Challenge and CAPTCHA carry an x-amzn-waf-action header.
Action What you get back What it means for a scraper
Allow 200 and the page Check the fields; a 200 can still be empty
Count 200 and the page Logged for the site owner, invisible to you
Block Usually 403 Forbidden, or a custom response Refused; slow down or change route
Challenge 202, x-amzn-waf-action: challenge, a page loading challenge.js Needs a real browser; not the page you asked for
CAPTCHA 405, x-amzn-waf-action: captcha A puzzle for a human; stop, don't retry blindly

The Challenge behaviour comes from ZenRows' AWS WAF write-up, whose captured page loads challenge.js from a token.awswaf.com subdomain and shows "JavaScript is disabled" in a <noscript> block. The CAPTCHA response comes from the AWS Security Blog.

How to tell what AWS WAF did

Read the response in order: the x-amzn-waf-action header first, then the page for a challenge.js script, then the status, and finally the fields you came for. This classifier does exactly that, and returns a verdict you can log and branch on. It needs only httpx and parsel.

# pip install httpx parsel
"""What did AWS WAF do with this request? Read the header first, then check the page."""
import sys

import httpx
from parsel import Selector


def waf_verdict(r: httpx.Response, item_css: str = "li.product", min_items: int = 5) -> str:
    action = r.headers.get("x-amzn-waf-action")          # set on Challenge and CAPTCHA responses
    if action:
        return f"{action} ({r.status_code})"              # e.g. "challenge (202)", "captcha (405)"
    if "token.awswaf.com" in r.text or "challenge-container" in r.text:
        return f"challenge page ({r.status_code})"
    if r.status_code == 403:
        return "blocked (403)"
    if r.status_code != 200:
        return f"error ({r.status_code})"
    if len(Selector(r.text).css(item_css)) < min_items:
        return "200 but no data"
    return "served"


if __name__ == "__main__":
    for url in sys.argv[1:] or [f"http://127.0.0.1:8774/p/{i}" for i in range(6)]:
        r = httpx.get(url)
        print(f"{url}  ok={r.is_success!s:<5}  ->  {waf_verdict(r)}")

Note the ok column it prints: on our lab edge, every 202 challenge showed ok=True while the verdict said challenge (202). The verdict string tells you which action fired, which tells you what to change.

  • x-amzn-waf-action present: a Challenge or CAPTCHA, whatever the status code says.
  • token.awswaf.com or challenge-container in the body: a challenge page, even without the header.
  • 403 with no header: most likely a Block rule, often from a rate-based rule.
  • 200 without your fields: treat it as a failure, not data.

The 202 trap: why scrapers store challenge pages

HTTP 202 means "Accepted", so most HTTP clients count it as success. requests' r.ok and raise_for_status() pass anything under 400, and httpx's is_success passes any 2xx. A scraper that relies on them stores AWS WAF's challenge page as if it were the product page you asked for.

We measured it on 40 responses from a local toy edge that copies AWS WAF's public response shapes: 20 real pages, 10 challenges (202), 6 CAPTCHAs (405) and 4 blocks (403). The scoring script is in the code download.

Isotype chart from our November 3, 2026 local lab, 40 responses from a toy edge with 20 real pages, 10 challenges (202), 6 CAPTCHAs (405) and 4 blocks (403): requests r.ok, raise_for_status and httpx is_success each kept the 20 real pages plus all 10 challenge pages; status == 200 and the x-amzn-waf-action plus items check kept only the 20 real pages
Our lab: three common success checks kept every 202 challenge page.

All three client success checks kept the 20 real pages plus all 10 challenge pages. A strict status == 200 check got it right here, but it can't tell you why a page failed, and it still passes a 200 that holds no data. Checking x-amzn-waf-action and the fields kept only the 20 real pages and named every failure.

"Every scraper we review on AWS WAF sites gets the same two-line change: read x-amzn-waf-action, then count the items. It turns silent bad data into a log line that says exactly what happened."

Rajesh Kannan, Co-Founder & CTO at Gramosoft

Tokens, immunity time and rate-based rules

When a browser passes a Challenge or CAPTCHA, AWS WAF sets an encrypted aws-waf-token cookie. The AWS Security Blog explains that it lasts for the immunity time the site sets, 300 seconds by default. When it expires, a protected path answers 405 with x-amzn-waf-action: captcha.

  • Tokens belong to the browser that earned them: they expire on the site's schedule, so a long run will meet fresh challenges.
  • Rate-based rules count requests over a window: AWS's own example counts requests per IP on a store path and shows a CAPTCHA above 100 requests in five minutes.
  • Escalation is the site's choice: Scrapfly notes that AWS WAF moves to a CAPTCHA when the challenge signals fail.

We don't cover CAPTCHA-solving services, copying or replaying aws-waf-token values, or stealth browsers that hide automation. They exist to defeat a control the site deliberately turned on, and a CAPTCHA in particular is the site asking for a human.

Pace requests and back off

Most AWS WAF trouble for well-behaved scrapers comes from rate-based rules, not clever detection. A rule that allows 100 requests in five minutes from one IP is easy to stay under if you plan for it, and expensive to fight if you don't. Slow, steady collection keeps you out of the Challenge and CAPTCHA paths.

  • Stay well under the likely limit: a few requests per second at most per site, lower on search and login paths.
  • Treat 202, 405 and 403 as "slow down": back off and wait longer after each one.
  • Never retry a CAPTCHA in a loop: it's an explicit request for a human, and retries just extend the block.
  • Honour robots.txt and cap concurrency per site.

What scraping protected pages costs

AWS WAF sites that challenge visitors need JavaScript rendering and usually premium IPs, so compare providers on their protected-page price, not their headline rate. For 100,000 successful protected pages a month, credit-based APIs cost $249 to $399 at list price and GcrawlAI costs $101. For plain pages, they're cheaper.

Option, 100,000 successful pages a month Plain HTML pages Protected pages (JS rendering + premium proxies) Are failed requests billed?
GcrawlAI (every request rendered, residential IPs included) $101 (Growth $79 + 20,000 overage at $1.10 per 1,000) $101 (same request, no multiplier) No: failed requests are never charged
ScraperAPI $49 (Hobby, 100,000 credits) $299 (Business; 25 credits per page = 2.5M credits) Charges only 200 and 404 responses
ZenRows $39 (Build B3, 120,000 credits) $399 (Growth G3; 25 credits per page = 2.5M credits) Failed requests never charged; 404 and 410 count as successful
ScrapingBee $49 (Freelance, 250,000 credits; JS off) $249 (Business; 25 credits per page = 2.5M credits) Charges only 200, 404 and 410

List-price arithmetic, not a benchmark. Credit costs and billing rules come from ScraperAPI's credit docs, ZenRows' pricing docs and ScrapingBee's FAQ, checked November 1, 2026. The honest read: for plain HTML, ZenRows, ScraperAPI and ScrapingBee cost less than GcrawlAI. Whatever you use, count cost per page with your fields in it, not per response.

GcrawlAI plans (GcrawlAI pricing, checked October 28, 2026):

Plan Price Requests/month Concurrent Per 1,000 Overage per 1,000
Free $0 forever, no card 50 2 – –
Starter $29/month 20,000 10 $1.45 $1.60
Growth (most popular) $79/month 80,000 35 $0.99 $1.10
Pro $199/month 250,000 50 $0.80 $0.90
Scale $499/month 750,000 100 $0.67 $0.75
Enterprise Volume pricing 2M+ Custom – –
Self-hosted $0 (MIT) Your servers Your servers Infrastructure only –

Every plan, Free included, has scrape, crawl, the Search API, JavaScript rendering, residential IP rotation and stealth escalation in each request, and each feature counts as one request. Failed requests are never charged. Annual billing charges 10 months for 12, unused requests on paid plans roll over once, and Indian customers are billed in INR with a GST invoice.

Which route fits your project

Start with the route that carries the least risk and upkeep, and move on only when it can't give you the data. For AWS WAF sites that order is usually an official source, then a managed browser API, then browsers you run yourself. The table shows where GcrawlAI fits in each case.

Your situation Route Where GcrawlAI fits
The site has an API, feed or partner programme Use it Not needed; use GcrawlAI for your other, unprotected sources
Only rate-based rules, no challenge Slow down and stay under the limit Spread steady volume through GcrawlAI with field checks
Challenge (202) on public pages you may collect A real browser GcrawlAI renders each page in a real browser, one request per page
CAPTCHA (405) on every visit Stop and ask for access Not a GcrawlAI job; the site wants a human
You need full control of the stack Self-host The MIT GcrawlAI core on your servers
AI agents need the page A tool the agent can call GcrawlAI MCP server with scrape, crawl and Search API

When your own Python is enough

Plenty of AWS WAF sites only rate-limit, and a slow, polite scraper never meets a challenge. For those, your own Python with the classifier above is all you need: it costs nothing, tells you the moment a site starts challenging, and keeps challenge pages out of your data whatever does the fetching.

When pages start coming back as 202 challenges and the content is public and allowed, move the fetching to GcrawlAI and keep your checks. Its free Cloud plan's 50 requests a month are enough to test your real targets, the Playground shows the result without code, and the MIT core self-hosts for free. For the build-or-buy trade-off, read web scraping with Python vs a scraping API.

Is it OK to scrape an AWS WAF-protected site?

A site that turns on Bot Control, Challenge or CAPTCHA rules is telling you it filters automated traffic. That doesn't make every collection unlawful, but it raises the bar. Read the terms and robots.txt, ask for an API or data licence, stay on public pages, and keep well under the site's rate limits.

Don't defeat the controls themselves: no CAPTCHA-solving services, no copying or replaying aws-waf-token values, and no exploiting rule gaps. Leave logins, checkouts and account pages alone, and don't collect personal data you have no basis to hold. For the legal background, read what web scraping is and where it's legal, and get advice for your own case.

"A CAPTCHA is the site asking for a person. When a customer's target shows one on every visit, we stop and help them ask for a feed instead. That conversation usually goes better than they expect."

Rajesh Kannan, Co-Founder & CTO at Gramosoft

Frequently asked questions

How do I know if a website uses AWS WAF?

Look for a 202 or 405 response with an x-amzn-waf-action header, a page that loads challenge.js from a token.awswaf.com subdomain, or an aws-waf-token cookie. A plain 403 Forbidden can also be an AWS WAF Block rule.

What does HTTP 202 with x-amzn-waf-action: challenge mean?

AWS WAF answered with its Challenge action: a page that must run challenge.js in a browser before the site is served. It isn't the page you asked for, even though 202 is a success code, so don't store it.

Why does my scraper store AWS WAF challenge pages?

Because requests' r.ok and raise_for_status() accept any status under 400, and httpx's is_success accepts any 2xx, so a 202 passes. In our lab each kept all 10 challenge pages. Check the header and your fields instead.

What is the aws-waf-token cookie?

The encrypted token AWS WAF sets after a browser passes a Challenge or CAPTCHA. It lasts for the site's immunity time, 300 seconds by default, after which protected paths challenge the visitor again.

What does a 405 from AWS WAF mean?

With x-amzn-waf-action: captcha, it means the CAPTCHA action fired, either because a rule requires it or because the visitor's token expired. It's a request for a human, so stop rather than retrying in a loop.

Can Python requests scrape an AWS WAF site?

On sites that only use IP or rate-based rules, yes, if you go slowly. On paths with the Challenge action, no: requests can't run challenge.js, so it only ever receives the 202 challenge page.

Is GcrawlAI open source, and can I use it commercially?

Yes. The GcrawlAI core is MIT licensed on GitHub, so you can self-host it and use it commercially. The managed Cloud API adds residential IPs and stealth escalation, with a Python SDK and MCP for AI agents, a free plan of 50 requests a month and paid plans from $0.67 to $1.45 per 1,000 requests.

RK

Rajesh Kannan M

Rajesh Kannan M is Co-Founder & CTO of Gramosoft in Chennai, where he leads the team building GcrawlAI, an open-source web scraping, crawling and Search API platform for developers and AI agents. LinkedIn

A 202 from AWS WAF isn't a page

Start with 50 free requests a month, no card. Send the protected pages you're allowed to collect through GcrawlAI, verify the content that comes back, and pay only for requests that succeed.

CTA Illustration