WPBench is a PHP-based benchmarking tool for WordPress and WooCommerce sites hosted anywhere. Give it a URL and it runs a full set of performance tests, including DNS, TLS, TTFB, HTTP timing, caching and header detection, and hosting provider identification. It also uses a real headless Chrome browser to measure page load performance and Core Web Vitals.
WPBench can run configurable load tests using either raw HTTP requests or full browser page loads to see how a site performs under sustained traffic and concurrency. Each test generates a self-contained HTML report, and you can compare 2–4 test runs side by side with metrics and charts to quickly see performance differences.
- HTTP timing: DNS lookup, connect, TLS handshake, TTFB, full download time, redirect chains. Standard and Thorough runs repeat the homepage fetch (2 and 3 times respectively) and average the results.
- Headers: full raw dump, plus detection of what's caching your site at the edge (Cloudflare, Fastly, and friends), at the webserver (Varnish, LiteSpeed, Nginx), and via WP caching plugins (WP Rocket, W3 Total Cache, and so on).
- Hosting provider fingerprinting: best effort identification of the managed WordPress host behind a response (WP Engine, Kinsta, Pantheon, SiteGround, Cloudways, Flywheel, Servebolt, WordPress.com/VIP, Automattic Pressable/Atomic) from known header and cookie signatures. A miss just means "couldn't tell," not "definitely not this host."
- Tech stack detection: server software/version, leaked PHP version,
response compression (Brotli/Gzip/Deflate/Zstandard), and a parsed
breakdown of any
Server-Timingheader the backend sends (useful for seeing what a host's own edge/cache layer reports internally). - WordPress and WooCommerce fingerprinting: theme and plugin
detection from public assets, generator tags, and the REST API;
WooCommerce presence detected via the
wc/v3/wc/store/v1REST namespaces or plugin asset paths. - Real browser metrics: full page load, First Contentful Paint, Largest Contentful Paint, Cumulative Layout Shift, DOM interactive time, browser-measured backend TTFB, first vs third-party resource breakdown (request count and bytes), and a screenshot, all captured with headless Chrome. Core Web Vitals get rated good/needs improvement/poor against Google's published thresholds and shown as badges in the report.
- Optional load test: a burst of concurrent requests to see how the host holds up under simultaneous visitors. Only runs on the Thorough complexity preset, and the concurrency level (how many simultaneous requests) is adjustable per run from the dashboard, from 1 up to a configurable ceiling (50 by default), so you can test the same host at different concurrency levels or match concurrency across hosts you're comparing.
- Stress test: its own complexity preset that sustains a fixed number of concurrent connections against the site for a configurable duration (5 to 60 seconds), refiring each connection the instant it completes so the concurrency level stays constant for the whole window, rather than firing one single wave like the Thorough load test does. Reports how many transactions completed successfully, how many failed, requests per second, and the fastest/slowest/mean/median page load time across the whole run, so you can see how a host holds up under sustained load rather than just an instant.
- Browser stress test: the same sustained load idea as Stress, but each concurrent "connection" is a real headless Chrome tab doing a full page load (HTML, CSS, JS, images, fonts, everything a real visitor's browser would fetch and render) rather than a bare HTTP request, so it measures render heavy sustained load instead of raw throughput. Every iteration clears cache and cookies first so each load behaves like a fresh visitor rather than benefiting from the browser's own caching. Since a real Chrome tab costs far more CPU and memory than a curl request, its default and maximum concurrency are both lower than the raw Stress preset.
- Optional WooCommerce add on: shop, product, and cart pages, plus a simulated add to cart and checkout page load.
| Preset | Homepage iterations | Profile pages | Browser metrics | Load test | Stress test |
|---|---|---|---|---|---|
| Quick | 1 | No | Yes | No | No |
| Standard | 2 | Yes | Yes | No | No |
| Thorough | 3 | Yes | Yes | Yes, adjustable concurrency (default 10, max 50) | No |
| Stress | 1 | No | No | No | Yes, raw HTTP, adjustable concurrency (default 20, max 100) and duration (5-60s, default 15s) |
| Browser Stress | 1 | No | No | No | Yes, real Chrome page loads, adjustable concurrency (default 5, max 50) and duration (5-60s, default 15s) |
Stress and Browser Stress both skip the profile page walk, headless browser metrics, and the Thorough load test entirely; the point of those presets is putting the host under sustained load, not a full page by page audit, so they go straight from the lightweight discovery steps (timing, headers, cache, tech stack, hosting provider) into the sustained burst.
Profile pages (Standard/Thorough) walk a small profile of extra pages
beyond the homepage: an internal content page for a plain WordPress run,
plus the shop/product/cart/checkout flow when the WooCommerce add on is
turned on. These are defined in config/profiles/wordpress.json and
config/profiles/woocommerce-addon.json.
Every completed run gets its own static report.html, written next to
its result.json and viewable at report.php?id=<run id> right from the
dashboard. Reports are self contained (the screenshot is embedded as a
base64 image) aside from linking back to the app's own stylesheet and a
small chart script, so nothing external is needed to view one. A report
includes: summary cards with Core Web Vitals rating badges, timing
breakdown, redirect chain, page resource breakdown, caching/tech stack
(including PHP version and compression), hosting provider, Server-Timing
breakdown, WordPress/WooCommerce detection, profile page timings, load
test results, stress test results (raw HTTP and/or real browser),
screenshot, and raw headers.
To compare runs, check 2 to 4 completed rows in the dashboard's history
table and hit "Compare selected". That takes you to
compare.php?ids=a,b,c, which lays out the key metrics side by side
(including hosting provider, server software/PHP version, compression,
third-party bytes, WooCommerce detection, and load test concurrency/avg/p95)
plus a bar chart of total load time.
- PHP 8.1 or newer, with the
curl,json,pcntl, andposixextensions. - Composer.
- A local Chrome or Chromium install (used headless for the browser based
metrics), via
chrome-php/chrome.
composer install
php -S localhost:8080 -t publicThen open http://localhost:8080 in your browser.
If Chrome/Chromium isn't on your PATH in a location chrome-php can
auto-detect, point at it explicitly with the WPBENCH_CHROME_BINARY
environment variable or the chrome_binary setting in
config/config.php.
WPBench also runs fine behind Apache with mod_php rather than the PHP built-in server, with two things worth knowing:
- The PHP process runs as whatever system user Apache runs as (often
apacheorwww-data), not your own user. Make suredata/runs/is writable by that user, e.g.chmod -R a+rwX data. - Headless Chrome's crash handler needs a writable
HOMEdirectory to start up at all; if Apache's service user has a read-only or non-existent home directory, the browser step fails immediately with a crashpad error. WPBench works around this itself by giving Chrome an isolated, writable temp profile directory andHOME/XDG_*env vars for every run, so no server configuration changes are needed for this specifically.
All runtime settings live in config/config.php:
data_dir/profiles_dir— where run data and profile JSON files live.chrome_binary/chrome_no_sandbox— headless Chrome setup.http_timeout/browser_timeout— request and page load timeouts.user_agent— the UA string WPBench identifies itself with.complexity_presets— iteration counts and load test toggle per preset.load_test_concurrency— default concurrency per preset (Thorough only by default).load_test_concurrency_max— hard ceiling on the concurrency a run can request from the dashboard, regardless of what's typed in. Exists so the burst test can't be turned into a real denial of service tool against whatever URL it's pointed at.stress_test— defaults and bounds for the Stress preset:default_concurrency/concurrency_max(20/100 by default) anddefault_duration_s/duration_min_s/duration_max_s(15s default, 5-60s range). Both concurrency and duration are adjustable per run from the dashboard and clamped server side to these bounds, for the same denial of service concern asload_test_concurrency_max, just doubly so since this one runs for a while instead of an instant.browser_stress_test— the same shape of defaults and bounds asstress_test, for the Browser Stress preset. Its default and maximum concurrency (5/50 by default) are lower than the raw Stress preset's, since a real Chrome tab costs far more CPU and memory than a curl request; raiseconcurrency_maxhere if your hardware can take more.
public/— the web front end (index.phpdashboard,compare.php,report.php) and JSON API endpoints (public/api/:start.php,status.php,abort.php,runs.php).src/Run/— run lifecycle: creating runs, reading/writing meta and status JSON, listing run history (RunManager), launching the background test process (ProcessLauncher).src/Runner/—TestRunner, which walks the step pipeline for a run and assemblesresult.json.src/Http/— the cURL based HTTP client used for all page fetches (CurlClient,CurlResponse,HeaderBag).src/Detect/— signature based detection:CacheDetector(edge/CDN, webserver, WP caching plugins),HeaderAnalyzer,TechDetector(server software, PHP version, compression, Server-Timing),HostingProviderDetector(managed WP host fingerprinting),WordPressDetector(theme/plugin/WooCommerce detection).src/Checks/— the individual test steps:ChromeBrowserCheck(headless browser metrics and screenshot),ProfilePagesCheck(profile page walk),LoadTestCheck(single wave concurrency burst for Thorough),StressTestCheck(sustained raw HTTP concurrency burst, used by the Stress preset),BrowserStressTestCheck(sustained real Chrome page load burst, used by the Browser Stress preset, orchestratingbin/browser-stress-worker.phpworker subprocesses since PHP can't drive more than one blocking Chrome navigation at a time), andLoadStats(the shared timing statistics all three burst checks report through).src/Report/—ReportGenerator(buildsreport.html) andCoreWebVitals(rates raw metrics against Google's thresholds).bin/— CLI entry points:run-test.php(the background test runner script launched byProcessLauncher) andbrowser-stress-worker.php(one concurrency slot's worth of sustained real Chrome navigation for the Browser Stress preset, launched byBrowserStressTestCheck).config/— app config (config.php) and test profile definitions (config/profiles/*.json).data/runs/— where run results and reports get written. Not tracked in git.tests/— PHPUnit tests, mirroring thesrc/namespace layout.
vendor/bin/phpunitGPL-3.0-or-later. See LICENSE for the full text. Copyright (C) 2026 Brad Boegler <bradthx@gmail.com>