Two protocols control Chrome for automation: WebDriver (W3C standard, used by Selenium) and Chrome DevTools Protocol (CDP, used by Puppeteer/Playwright). Each has different detection profiles, capabilities, and CAPTCHA handling characteristics.
Protocol Architecture
WebDriver (Selenium):
Script ──HTTP──▶ ChromeDriver ──DevTools──▶ Chrome
(JSON Wire) (bridge) (internal)
CDP (Puppeteer/Playwright):
Script ──WebSocket──▶ Chrome
(direct) (no middle layer)
Head-to-Head Comparison
| Feature | WebDriver | CDP |
|---|---|---|
| Protocol | HTTP + JSON | WebSocket |
| Standard | W3C standard | Chrome-specific |
| Detection surface | High (navigator.webdriver = true) |
Lower (patchable) |
| Network interception | No (needs Selenium Wire) | Built-in (Fetch, Network) |
| JavaScript evaluation | executeScript (wrapped) |
Runtime.evaluate (direct) |
| Request modification | No | Yes |
| Response modification | No | Yes |
| Connection overhead | HTTP per command | Persistent WebSocket |
| Cross-browser | Chrome, Firefox, Safari, Edge | Chrome/Chromium only |
| Stealth potential | Low (always sets webdriver flag) | High (flag patchable) |
Detection Differences
WebDriver Detection (Selenium)
// What sites check for Selenium
navigator.webdriver // true (set by ChromeDriver)
document.$cdc_asdjflasutopfhvcZLmcfl // ChromeDriver internal variable
window.callSelenium // Older Selenium versions
window._selenium // Selenium internals
document.__webdriver_evaluate // WebDriver namespace
# Even with flag removal, ChromeDriver leaves traces
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
driver = webdriver.Chrome(options=options)
# Still detectable via $cdc variables in document
CDP Detection (Puppeteer)
// Puppeteer sets webdriver flag too, but it's fully patchable
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
// No $cdc variables to worry about
// No Selenium-specific traces
});
CAPTCHA Solving: WebDriver Approach
# Selenium (WebDriver) + CaptchaAI
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
API_KEY = "YOUR_API_KEY"
API_URL = "https://ocr.captchaai.com"
options = webdriver.ChromeOptions()
options.add_argument("--disable-blink-features=AutomationControlled")
driver = webdriver.Chrome(options=options)
driver.get("https://example.com/form")
time.sleep(2)
# Extract sitekey
sitekey = driver.execute_script(
"return document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')"
)
# Solve via CaptchaAI
resp = requests.post(f"{API_URL}/in.php", data={
"key": API_KEY, "method": "userrecaptcha",
"googlekey": sitekey, "pageurl": driver.current_url, "json": 1,
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{API_URL}/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": 1,
})
data = resp.json()
if data["request"] != "CAPCHA_NOT_READY":
break
token = data["request"]
# Inject token
driver.execute_script(f"""
document.querySelector('#g-recaptcha-response').value = '{token}';
document.querySelectorAll('[name="g-recaptcha-response"]')
.forEach(el => {{ el.value = '{token}'; }});
""")
driver.find_element(By.CSS_SELECTOR, "form").submit()
driver.quit()
CAPTCHA Solving: CDP Approach
// Puppeteer (CDP) + CaptchaAI
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
const https = require("https");
puppeteer.use(StealthPlugin());
const API_KEY = "YOUR_API_KEY";
const API_URL = "https://ocr.captchaai.com";
function solveCaptcha(siteUrl, sitekey) {
// Submit + poll (simplified)
return new Promise(async (resolve, reject) => {
const submitResp = await httpPost(`${API_URL}/in.php`, {
key: API_KEY, method: "userrecaptcha",
googlekey: sitekey, pageurl: siteUrl, json: "1",
});
const taskId = submitResp.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const result = await httpGet(
`${API_URL}/res.php?key=${API_KEY}&action=get&id=${taskId}&json=1`
);
if (result.request !== "CAPCHA_NOT_READY") {
return resolve(result.request);
}
}
reject(new Error("Timeout"));
});
}
(async () => {
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--window-size=1920,1080"],
});
const page = await browser.newPage();
// CDP: Enable network interception
const cdp = await page.createCDPSession();
await cdp.send("Network.enable");
// Monitor CAPTCHA-related requests
cdp.on("Network.requestWillBeSent", (params) => {
if (params.request.url.includes("recaptcha")) {
console.log(`CAPTCHA request: ${params.request.url}`);
}
});
await page.goto("https://example.com/form", { waitUntil: "networkidle0" });
const sitekey = await page.evaluate(() =>
document.querySelector("[data-sitekey]")?.getAttribute("data-sitekey")
);
if (sitekey) {
const token = await solveCaptcha(page.url(), sitekey);
// CDP: Inject via Runtime.evaluate (lower-level than page.evaluate)
await cdp.send("Runtime.evaluate", {
expression: `
document.querySelector('#g-recaptcha-response').value = '${token}';
document.querySelector('form').submit();
`,
});
}
await browser.close();
})();
Capability Comparison for CAPTCHA Workflows
Network Interception
# WebDriver: Cannot intercept requests natively
# Requires selenium-wire (third-party)
from seleniumwire import webdriver
driver = webdriver.Chrome()
# selenium-wire proxies all traffic through a local proxy
// CDP: Built-in request interception
await cdp.send("Fetch.enable", {
patterns: [{ urlPattern: "*recaptcha*" }],
});
cdp.on("Fetch.requestPaused", async ({ requestId, request }) => {
console.log(`Intercepted: ${request.url}`);
await cdp.send("Fetch.continueRequest", { requestId });
});
Response Modification
// CDP: Can modify responses (WebDriver cannot)
cdp.on("Fetch.requestPaused", async ({ requestId, request }) => {
if (request.url.includes("captcha-config")) {
// Return modified response
await cdp.send("Fetch.fulfillRequest", {
requestId,
responseCode: 200,
body: btoa(JSON.stringify({ enabled: false })),
});
} else {
await cdp.send("Fetch.continueRequest", { requestId });
}
});
Cookie Manipulation
# WebDriver: Standard cookie API
driver.add_cookie({"name": "session", "value": "abc123"})
cookies = driver.get_cookies()
// CDP: Full cookie control including httpOnly
await cdp.send("Network.setCookie", {
name: "session",
value: "abc123",
domain: "example.com",
httpOnly: true,
secure: true,
});
// Get all cookies including httpOnly (WebDriver can't access these)
const { cookies } = await cdp.send("Network.getAllCookies");
Decision Matrix
| Scenario | Recommended | Why |
|---|---|---|
| Multi-browser testing | WebDriver | Cross-browser support |
| Maximum stealth | CDP | Lower detection surface |
| Network debugging | CDP | Built-in interception |
| Team unfamiliar with CDP | WebDriver | Simpler API, more docs |
| High-scale scraping | CDP | Lower overhead per session |
| CI/CD integration | WebDriver | Better tooling support |
| CAPTCHA-heavy sites | CDP + CaptchaAI | Stealth + solve combo |
| Simple form filling | WebDriver + CaptchaAI | Easier to implement |
Hybrid Approach: WebDriver + CDP
Selenium 4 supports CDP commands through BiDi (bidirectional):
from selenium import webdriver
driver = webdriver.Chrome()
# Use WebDriver for navigation
driver.get("https://example.com")
# Use CDP for stealth patches
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
"""
})
# Use CDP for network events
driver.execute_cdp_cmd("Network.enable", {})
Performance Benchmarks
| Metric | WebDriver (Selenium) | CDP (Puppeteer) | CDP (Playwright) |
|---|---|---|---|
| Session start | ~2.5s | ~1.5s | ~1.2s |
| Page navigation | ~500ms overhead | ~100ms overhead | ~80ms overhead |
| Script execution | ~200ms per call | ~50ms per call | ~40ms per call |
| Memory per session | ~350 MB | ~280 MB | ~260 MB |
| CAPTCHA solve (CaptchaAI) | 15-30s | 15-30s | 15-30s |
CaptchaAI solve time is identical — it's an API call independent of the browser protocol.
Troubleshooting
| Issue | WebDriver Fix | CDP Fix |
|---|---|---|
| Detected as bot | excludeSwitches + options |
Stealth plugin + patches |
| Can't intercept requests | Use selenium-wire | Use Fetch.enable |
| Slow execution | Reduce execute_script calls |
Batch CDP commands |
| Token injection fails | Check iframe context | Use Runtime.evaluate with context |
| Session instability | Update ChromeDriver | Check WebSocket connection |
FAQ
Which protocol gives better CAPTCHA success rates?
Success rates are identical — CaptchaAI solves server-side. The protocol only affects detection frequency (how often CAPTCHAs appear).
Can I mix WebDriver and CDP?
Yes. Selenium 4 exposes execute_cdp_cmd(). Use WebDriver for navigation and CDP for stealth patches.
Is Playwright better than both?
Playwright uses CDP internally but provides a higher-level API with auto-wait and better error handling. It's a good middle ground.
Should I switch from Selenium to Puppeteer for CAPTCHA work?
If detection is your main problem, yes. If you need cross-browser support, stay with Selenium + CaptchaAI.
Related Guides
Choose the right automation protocol for your CAPTCHA workflow — get your CaptchaAI key and solve CAPTCHAs regardless of protocol.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.