Skip to content
Detection teardowns

Proof of work as a core-count check: what a challenge learns from your hash rate

A challenge that starts one worker per claimed core and times them gets a direct measurement of how much CPU the session really has.

Pim
Pim· Clearcote Research
6 min read

Key takeaways

  • A deployed challenge starts min(32, max(1, ceil(hardwareConcurrency × 0.75))) SHA-256 workers, gives each a fixed time budget, and reports how many hashes they computed and the best result.
  • The hash count measures real parallel CPU capacity, so it has to fit the claimed hardwareConcurrency and the speed of a separate four-kernel WebAssembly benchmark.
  • Hosts that run many browsers at once, and sessions that claim more cores than the machine gives them, report hash rates no single real device of that kind produces.

What the proof of work does

Many challenges include a proof of work: the browser searches for a nonce whose SHA-256 digest, together with a server-supplied prefix, starts with enough zero bits. Its classic purpose is cost. Each attempt takes CPU time, so mass attempts become expensive.

One deployed widget's version is time-bounded instead of target-bounded. It starts min(32, max(1, ceil(hardwareConcurrency × 0.75))) dedicated workers, each with its own nonce range and a fixed time budget that starts when the worker's script begins. When the budget runs out, every worker reports the best digest it found and how many digests it computed. The answer is therefore not only a proof but a measurement: how many hashes this browser did in a known amount of time, across as many threads as it said it had.

What the hash rate reveals

The number of workers comes from the session's own claim. A session reporting 16 logical cores starts 12 workers; one reporting 4 starts 3. If the machine really has those cores free, the total grows with the worker count. If it does not, because the session claims more cores than it has, or because many browsers share one host, the workers compete for the same cores and the total stays flat while the count grows.

Per-worker speed matters too. The same widget runs a second worker that times four arithmetic kernels compiled to WebAssembly, plus the time WebAssembly.instantiate took. That gives a single-thread speed figure for the CPU. Divide the proof's total by the worker count and the per-worker rate should match what that single-thread speed predicts. A fast single thread with a slow per-worker hash rate means the workers were starved.

Neither measurement needs a reference database. Both are internal comparisons: the claim against the throughput, and one benchmark against another, as in cross-realm coherence.

Where it catches automation

  • Dense hosts. A server running dozens of browsers presents each one with a plausible desktop core count, but each gets a fraction of the CPU. The proof sees a high claimed core count with the throughput of one or two cores.
  • Containers with CPU limits. A container limited to two CPUs on a 64-core host may still report the host's core count, or a persona's. Either way the workers share two CPUs.
  • Virtual machines under load. CPU time taken by neighbouring guests can show up as uneven hash counts between the workers of one proof.
  • Throttled background tabs. A page that is not visible can have its timers and workers slowed; the widget records document visibility before it starts the benchmark.

None of this needs a JavaScript patch to be present. The measurement is physical, which is why it is hard to answer from inside the page.

What follows for automation

The claimed core count and the CPU actually available have to be chosen together. A session on a shared host should claim a core count the host can serve to it, and the number of concurrent browsers per host is a fingerprint decision, not only a cost decision. Timing also has to be real: the proof's budget is measured with performance.now() in the worker, so the clock has to behave like the claimed browser's, with Chrome's 100 µs steps for example.

The same reasoning applies to anything that measures performance: frame timing, animation callbacks and audio rendering all take longer on a starved CPU. See how automation gets caught, layer by layer for where these timing signals sit next to the others.

The through-line

Most fingerprint values are things the browser says. The proof of work is something the browser does, and its result is bounded by physics. That makes it the clearest case of the principle in coherence over camouflage: a claim that the hardware cannot back up shows up as a contradiction, whatever value the claim itself reports. The full probe list is in inside a challenge widget.

#bot-detection#coherence#reference

Clearcote puts this into practice

An open-source Chromium with fingerprint control compiled into the engine. A drop-in for Playwright & Puppeteer.

  • 222engine patches
  • 34,478lines added
  • 330Chromium files changed
  • 354checks per build

Engine numbers are for the Pro build; the free open build is a smaller, public patch set.

Free for one browser with GitHub. No card.