Skip to content

HashWX ​

HashWX (designed by tevador) is Cap's default proof of work. It's gpu-resistant -- where SHA-256 uses one fixed hash function, HashWX generates a new one for every block of nonces, built from integer operations and branches that GPUs run poorly.

Why not SHA-256 ​

What matters for bot protection is how many challenges an attacker clears per hour, not how long one takes. A GPU runs a fixed function like SHA-256 on thousands of lanes at once, while HashWX is designed to run on a single lane. Measured against a consumer GPU:

AlgorithmCPU, Ryzen 3700X, 16 threadsGPU, RTX 5060 TiGPU advantage
SHA-25641 MH/s6150 MH/s~150x
HashWX2.8 MH/s5.8 MH/s~2x

Cost ​

Server time per challenge on one Apple M3 core, median of 200 mints and 40 verifications:

ProtocolMintVerifyTotal
HashWX, 1 challenge14 µs40 µs54 µs
HashWX, 4 sub-challenges (default)20 µs129 µs149 µs
SHA-256, 50 challenges at difficulty 44 µs83 µs87 µs

Difficulty doesn't change these numbers, since verifying is one hash per sub-challenge however long it took to find.

On the client, one challenge has a random solve time: the same difficulty can take 30 ms for one visitor and 3 s for the next. Cap splits the difficulty into four sub-challenges by default, which evens it out. Measured through the widget in Chrome on an 8-core M3, 72 solves each, with the two difficulties set for about the same median:

1 challenge, d = 1,330,0004 sub-challenges, d = 1,000,000
Median536 ms490 ms
p901447 ms778 ms
Slowest of 722378 ms1490 ms

The right column is the default. Against a capd key it measured a 578 ms median and a 0.9 s p90. Splitting doesn't change what an attacker pays, since the expected work is d hashes either way.

Browsers ​

The WebAssembly build runs at about 60% of native speed. Measured on the same M3:

Browser1 worker8 workersInterpreted fallback
Chrome 153440 KH/s2050 KH/s94 KH/s
Firefox 156420 KH/s1850 KH/s105 KH/s
Safari 27.2410 KH/s1480 KH/s105 KH/s

Safari reaches about 70% of Chrome's speed on all cores, so tune difficulty against Safari. At the default, that's about 0.5 s on Chrome and 0.7 s on Safari.

If you measure this yourself, use release builds with the tab in front. Playwright's Firefox runs all WebAssembly three to six times slower than real Firefox, and a background tab can end up on efficiency cores.

Phones ​

The default difficulty on real phones through BrowserStack, 15 solves each, each starting from a freshly loaded page:

DeviceOSAll coresMedianSlowest
Galaxy S24Android 141238 KH/s1.1 s1.8 s
Pixel 9Android 15837 KH/s1.4 s2.0 s
Pixel 6Android 12746 KH/s1.9 s2.4 s
iPhone 15iOS 17not measured1.9 s4.3 s
iPhone 12iOS 17620 KH/s2.0 s3.3 s
iPhone 13iOS 15678 KH/s2.2 s4.1 s
iPhone SE 2022iOS 15588 KH/s2.4 s4.0 s
Redmi Note 11Android 11456 KH/s2.4 s4.8 s

Each solve includes two round trips to the test server, 80 to 190 ms each. A page that had already been running HashWX for a minute solved 24% to 29% faster, so benchmarks that warm up first look better than what visitors get.

If most of your traffic is mobile, lower the difficulty. Solve time scales with it, so 500,000 roughly halves the solving part of these numbers.

HashWX needs WebAssembly, and iOS 15 or newer on iPhones. For browsers without WebAssembly, use SHA-256, which has a JavaScript fallback.

Limits ​

  • Custom hardware would still beat a CPU. An FPGA or ASIC built for HashWX costs millions to design, which makes no sense for solving CAPTCHAs, but that's an economic argument and not a cryptographic one.

  • Botnets could theoretically solve HashWX faster, but most botnets consist of extremely low-end hardware like old cameras that can't solve HashWX at any reasonable speeds. They're overkill here as well.

  • HashWX can still be solved by a server. Pair it with instrumentation so a real browser is required too.