CAPTCHA: What It Is and Why Screenshots Hit One

A CAPTCHA is a challenge that separates people from bots. The main types, why automated screenshots hit them, and tested code that fails the capture.

By Simon Chiu · Updated

A CAPTCHA is a challenge a website shows to tell a human visitor from an automated program, either as a visible task (pick the traffic lights, tick a box) or as an invisible check that runs in the background and scores the visitor.

The name stands for "Completely Automated Public Turing test to tell Computers and Humans Apart". For automated screenshots, a CAPTCHA is the most common reason a capture returns HTTP 200 and the wrong picture. This entry is part of the screenshot glossary.

What it means in plain terms

A CAPTCHA is the result of bot detection, not the detection itself. When a site is unsure about a visitor, it puts a gate in front of the page. A person passes the gate in a few seconds. A headless browser waits at it until the capture times out, or, worse, takes a picture of the gate and reports success.

The main types

Type What the visitor sees What a screenshot usually shows
Image or text puzzle (classic, reCAPTCHA v2 image grid) A task to solve The puzzle
Checkbox ("I'm not a robot", hCaptcha) One click, sometimes followed by a puzzle The checkbox widget in an iframe
Invisible score (reCAPTCHA v3) Nothing The real page, but the site may block the next action
Interstitial challenge (for example Cloudflare's managed challenge, often with Turnstile) "Just a moment..." or "Verify you are human" for a few seconds The interstitial, with the page never loaded

The last row is what most screenshot jobs meet, because it sits in front of the whole page rather than one form.

How it works technically

 headless browser ──GET /product/42──▶ edge / bot product
                                          │ score: doubtful
                                          ▼
                    ◀── 200 (or 403) "Verify you are human" + challenge script
 challenge script runs browser checks, may show a widget (iframe from the CAPTCHA provider)
          │ pass: sets a clearance cookie, reloads the real page
          │ fail or never finishes: you stay on the challenge
          ▼
 screenshot ──▶ the real page   or   the challenge (HTTP 200, looks like success)

Two details matter for automation. The widget usually lives in an iframe from the provider's domain, which you can detect. And a passed challenge is stored as a cookie in that browser context; a fresh context for every capture means a fresh challenge every time.

How to handle it

Do not try to solve it. Make the capture fail loudly, so you do not store or pay for a picture of a gate.

Puppeteer: detect the widget or the text

This checks frame URLs for common CAPTCHA providers and the page text for challenge phrases. On https://example.com it saved the screenshot. On a local page with the text "Verify you are human" it printed the reason and exited with code 2:

import puppeteer from 'puppeteer';

const WIDGET_HOSTS = ['google.com/recaptcha', 'recaptcha.net', 'hcaptcha.com', 'challenges.cloudflare.com'];
const CHALLENGE_TEXT = ['Verify you are human', 'unusual traffic', 'Access denied'];

const url = process.argv[2] ?? 'https://example.com/';
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2' });

  const widget = page.frames().map((f) => f.url()).find((u) => WIDGET_HOSTS.some((h) => u.includes(h)));
  const text = await page.evaluate(() => document.body?.innerText ?? '');
  const phrase = CHALLENGE_TEXT.find((p) => text.includes(p));

  if (widget || phrase) {
    console.error(`CAPTCHA or block page on ${url}: ${widget ?? phrase}`);
    process.exitCode = 2; // fail the job; do not store the image
  } else {
    await page.screenshot({ path: 'captcha-check.png' });
    console.log('no challenge found, saved captcha-check.png');
  }
} finally {
  await browser.close();
}

Playwright: require proof of the real page

A deny-list of phrases always lags behind new challenge pages. The sturdier check is positive: wait for an element only the real page has. Pass your own selector as the second argument:

import { chromium } from 'playwright';

const url = process.argv[2] ?? 'https://example.com/';
const proof = process.argv[3] ?? 'body > p'; // an element only the real page has

const browser = await chromium.launch();
try {
  const page = await browser.newPage();
  await page.goto(url);
  try {
    await page.locator(proof).first().waitFor({ state: 'visible', timeout: 15_000 });
  } catch {
    const title = await page.title();
    throw new Error(`"${proof}" never appeared on ${url} (title: "${title}"). Likely a challenge page.`);
  }
  await page.screenshot({ path: 'captcha-ok.png' });
  console.log('real content found, saved captcha-ok.png');
} finally {
  await browser.close();
}

Against the local challenge page with the selector main, it threw "main" never appeared ... (title: "Just a moment...") after 15 seconds. Tested on 2026-10-15 with Node 22.18.0, [email protected] (run with headless: 'shell') and [email protected]. The local page imitated challenge text; we did not test against a real CAPTCHA provider.

RenderScreenshot

We do not solve CAPTCHAs, and the renderer does not hide that it is automated. The bot protection guide explains why. What the API offers is a content check, so a challenge page fails with content_check_failed (HTTP 422) and uses no credit:

curl -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://shop.example.com/product/42",
    "options": {
      "fail_if_contains": ["Verify you are human", "Just a moment", "unusual traffic"],
      "fail_if_missing": ["Add to cart"]
    }
  }' \
  --output product.png

Matching is case-sensitive, so copy the phrases from challenge pages you actually received. Do not retry these failures in a loop; the retries guide suggests a cap of one. This request was not run for this page.

Common mistakes

  • Counting HTTP 200 as success. Interstitial challenges often return 200.
  • Retrying until it passes. Repeated attempts from the same source tend to raise the risk score.
  • Confusing a CAPTCHA with a consent banner. A cookie consent banner can be dismissed; a CAPTCHA cannot be by design.
  • Waiting longer. A longer wait does not let a challenge pass; it only makes the failure slower.
  • Ignoring the owner's choice. For a site you do not control, a CAPTCHA is a clear "no". For your own site, allow the renderer through instead.

To make challenge pages fail instead of using credits, get a free API key and add the content check above.

Frequently asked questions

What does CAPTCHA stand for?
Completely Automated Public Turing test to tell Computers and Humans Apart. The term was coined at Carnegie Mellon University in the early 2000s.
Does RenderScreenshot solve CAPTCHAs?
No. We do not solve CAPTCHAs or send them to solving services. Use a content check so a challenge page fails the request instead of using a credit, and ask the site owner to let captures through.
Why did my screenshot show 'Just a moment...' or 'Verify you are human'?
The site's bot protection served an interstitial challenge instead of the page. The capture worked; it captured the challenge. Detect the text or wait for an element that only the real page has.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.