axe-core vs Pa11y vs Deque WorldSpace for Enterprise Batch Scanning
Pick axe-core when you want a deterministic engine to embed in your own harness, Pa11y when you want a zero-harness CLI runner for quick coverage, and Deque’s axe DevTools Pro / WorldSpace when you need coverage beyond the open rules plus guided human testing and a support contract — the three are not interchangeable because they sit at different layers of the stack. This guide resolves the specific decision of which to standardize on for scanning thousands of routes, and sits within the Scanner Tool Selection and Benchmarking topic area under Automated Scanning & Dynamic Content Ingestion. It assumes you have already framed the selection axes there and now need the concrete three-way trade-off with integration code for each.
When This Applies
Reach for this comparison when you are choosing one primary tool for an enterprise batch program and the shortlist is these three. The distinction only matters at scale and under specific constraints:
- You are scanning hundreds to thousands of routes, so throughput and determinism dominate over convenience.
- A meaningful share of your surface is authenticated, so session support is a hard filter.
- You have a budget question: build-and-maintain an open-source harness, or buy a commercial suite with support.
- You need the output to normalize cleanly into one downstream pipeline.
If you are a single developer spot-checking one page, any of the three works and the decision does not matter — reach for whichever is already installed.
Minimal Reproducible Example
The layering is easiest to see by what each tool requires you to write for one scan. axe-core gives you an engine but no runner, so the naive attempt to “just run axe” against a URL does nothing on its own:
// broken-intent.js — axe-core is a library, not a runner. This does not scan a URL.
const axe = require('axe-core');
// axe.run() evaluates the DOM of the page it is INJECTED into. There is no
// page here — no browser, no navigation — so there is nothing to evaluate.
axe.run().then(results => console.log(results.violations)); // ReferenceError: document is not definedaxe-core has to be injected into a real, hydrated page by a browser driver you supply. Pa11y and Deque’s tooling bundle that driver for you — which is precisely the trade-off between control and convenience.
Correct Implementation
Below is the smallest correct integration for each tool, so the differences are concrete rather than described. Each scans one route and yields findings you would then normalize.
axe-core, driven by Playwright — you own the browser lifecycle and inject the engine:
# axe_scan.py — you supply the driver; axe supplies the rules. Fully deterministic.
from playwright.sync_api import sync_playwright
def scan_with_axe(url: str, storage_state=None) -> dict:
with sync_playwright() as p:
browser = p.chromium.launch()
# Injecting storage_state is how axe reaches authenticated routes.
ctx = browser.new_context(storage_state=storage_state)
page = ctx.new_page()
page.goto(url, wait_until="networkidle") # your harness owns DOM-readiness
page.add_script_tag(path="node_modules/axe-core/axe.min.js") # pinned engine
results = page.evaluate("() => axe.run(document, "
"{runOnly:{type:'tag',values:['wcag2a','wcag2aa']}})")
browser.close()
return results # structured JSON: violations[].nodes[], stable rule idsPa11y — the runner drives its own Chromium and wraps an engine; you point and shoot:
// pa11y_scan.js — Pa11y bundles Puppeteer's Chromium and wraps axe or HTML_CodeSniffer.
const pa11y = require('pa11y');
async function scanWithPa11y(url) {
const results = await pa11y(url, {
runners: ['axe'], // choose the wrapped engine: 'axe' or 'htmlcs'
standard: 'WCAG2AA',
// Authenticated routes need explicit login actions or an injected cookie —
// Pa11y has no native storage_state, so this is scripted per run.
actions: [
'set field #username to svc-scanner',
'set field #password to ${SCAN_SECRET}',
'click element button[type=submit]',
'wait for path to be /dashboard',
],
});
return results.issues; // flat array: {code, message, selector, type, runner}
}Deque axe DevTools Pro / WorldSpace — the commercial suite runs the extended engine and persists results via its API:
# deque_scan.py — sketch. The suite runs axe plus proprietary rules and saves
# results server-side; you retrieve them through its API rather than parsing stdout.
import os, requests
def scan_with_axe_devtools(url: str) -> dict:
# The Pro engine (a superset of open axe-core rules) is injected by the suite's
# own driver; guided IGT flows are triggered separately for non-automatable SCs.
resp = requests.post(
"https://axe.deque.com/api/v1/scans",
headers={"Authorization": f"Bearer {os.environ['DEQUE_API_KEY']}"},
json={"url": url, "standard": "WCAG21AA", "save": True},
timeout=120,
)
resp.raise_for_status()
return resp.json() # saved run with history, retrievable and diffable via APIThe ${SCAN_SECRET} and API-key patterns above assume secrets are injected from your CI vault, never committed. The three snippets make the layering obvious: axe-core is an engine you drive, Pa11y is a driver that wraps an engine, and the Deque suite is a managed service around an extended engine.
Pipeline Integration Note
Whichever tool wins, it feeds the same downstream contract. axe-core’s structured JSON maps almost directly onto the normalized finding record enforced by JSON Schema validation for accessibility data, which is a large part of why it is the default embed; Pa11y’s flatter issue array and Deque’s API payload each need an adapter to reach the same shape. Once normalized, findings from any of the three shard through the batch validation architecture and route to owners via the error categorization triage pipelines. The engine you standardize on is configured once in the axe-core enterprise configuration — a settings surface that axe-core exposes directly, Pa11y exposes partially through its options, and the Deque suite manages for you.
The side-by-side that actually drives the decision:
| Axis | axe-core | Pa11y | Deque axe DevTools Pro / WorldSpace |
|---|---|---|---|
| What it is | JS engine you inject and call axe.run() |
Node CLI runner wrapping axe or HTML_CodeSniffer | Commercial suite built on an extended axe engine |
| Browser | You supply (Playwright/Puppeteer) | Bundled Puppeteer Chromium | Managed by the suite |
| Rule coverage | Open WCAG 2.2 rule set | Depends on wrapped engine | Superset — proprietary rules beyond open axe |
| Determinism | High — lockfile-pinned engine | Medium — pin the Pa11y and engine versions | High, but version controlled by the vendor |
| Authenticated state | Full — inject storage_state |
Scripted login actions, no native state | Supported, configured in the suite |
| Guided/manual testing | None — automation only | None | Guided IGT for non-automatable SCs |
| Output shape | Structured JSON, stable rule ids | Flat issue array | Saved runs with history via API |
| Cost | Free (license) | Free (license) | Paid per seat / scan + support |
| Best fit | Deterministic CI gate at scale | Quick coverage, minimal harness | Large program needing coverage + support |
Read the table as elimination-first. If authenticated coverage is mandatory and you cannot maintain scripted logins, Pa11y drops out. If determinism must be under your control rather than a vendor’s release train, that weighs against the suite for the gate specifically. If you have no appetite to build and own a harness, axe-core-raw drops out in favor of Pa11y or the suite.
Pick axe-core when the tool must back a CI gate: you want the engine pinned in your lockfile, injected into a browser you control, emitting JSON that normalizes without an adapter. It is the most work up front and the most control forever.
Pick Pa11y when you want broad coverage fast with almost no harness, the routes are mostly public or have simple logins, and you can tolerate a flatter output that needs a small adapter. It is the pragmatic choice for a program that is not yet ready to own a full scanning harness.
Pick Deque’s suite when the program is large enough that coverage beyond the open rules, guided testing for the criteria a machine cannot decide, saved history via an API, and a support contract are worth per-seat cost — and when you would otherwise be rebuilding those capabilities yourself.
Gotchas
- Pa11y’s authenticated scanning is fragile at scale. Scripted login actions run per route and hit rate limits or flake under load; there is no native
storage_stateto reuse a session. For an authenticated batch program, capture a session once and inject it — a pattern axe-core-on-Playwright supports natively but Pa11y does not. - The Deque suite’s rule superset changes your baseline counts. Because axe DevTools Pro runs proprietary rules beyond open axe-core, migrating to it from raw axe-core will surface new findings that are real but previously invisible — plan a re-baselining pass so the delta is not mistaken for a regression.
- Multi-tenant routing can silently scan the wrong shell across all three. A runner hitting a bare host may resolve to a marketing or default-tenant shell and report a reassuring zero. Pin the tenant by host header or path prefix and assert on a tenant-specific landmark before trusting any tool’s count.
Frequently Asked Questions
Is Pa11y just axe-core with a command line on top?
Partly. Pa11y is a runner that can wrap axe-core as one of its engines, but it can also run HTML_CodeSniffer, and it supplies its own bundled browser and result format. So a Pa11y-with-axe run is close to a raw axe run in coverage, but the invocation, session handling, and output shape differ, and those differences are what matter for a batch pipeline.
Does Deque WorldSpace find more real issues than open axe-core?
It runs a superset of rules including proprietary checks, so it can surface findings open axe-core does not, and its guided intelligent testing covers criteria no automated engine can decide. Whether that extra coverage justifies the cost depends on your program size and whether you would otherwise build equivalent human-in-the-loop workflows yourself.
Can we mix them — Pa11y in CI and Deque for audits?
Yes, and it is a common split: a free deterministic engine gates every merge while the commercial suite drives periodic deep audits with guided testing. The cost is a normalization layer that reconciles two output shapes into one record, and discipline to keep the gate’s engine version pinned independently of the audit tool.
Which one holds an authenticated session most reliably?
axe-core driven by Playwright, because you inject a captured storage_state and every route reuses the same session without a live login. Pa11y scripts login actions per run, which flakes and rate-limits at scale, and the Deque suite supports authentication through its own configuration. For a large authenticated batch program the injected-state model is the most robust.