Skip to content
Anti-Bot

What Is a Proof-of-Work Challenge? Hash Puzzles as a CPU Measurement

Pim

Pim · Scrappey Research

October 28, 2026 4 min read

What Is a Proof-of-Work Challenge? Hash Puzzles as a CPU Measurement — conceptual illustration
On this page

A proof-of-work challenge makes the browser search for a value whose hash meets a condition, usually a SHA-256 digest that starts with enough zero bits, before the site lets it continue. Its original purpose is cost: each attempt takes CPU time, so mass requests become expensive. A newer use is measurement: when the challenge runs one worker per claimed CPU core for a fixed time and reports how many hashes they computed, the result shows how much CPU the session really has.

Quick facts

Typical puzzleFind a nonce so SHA-256(prefix + nonce) starts with N zero bits
Original purposeMake each request cost CPU time
Measurement useHashes per fixed time budget across the claimed number of cores
Worker count in one deployed widgetmin(32, max(1, ceil(hardwareConcurrency x 0.75)))
Open-source exampleAnubis (target-bounded puzzle in front of a site)

Target-bounded and time-bounded puzzles

There are two common designs. A target-bounded puzzle sets a difficulty and lets the browser work until it finds a nonce that meets it. The open-source Anubis gatekeeper works this way: a slow device takes longer, but every visitor eventually gets through, and a crawler pays the CPU cost once per session.

A time-bounded puzzle sets a time budget instead. One widely deployed challenge widget, observed in October 2026, starts min(32, max(1, ceil(hardwareConcurrency x 0.75))) dedicated workers, gives each its own nonce range and a fixed budget, and when time runs out reports each worker's best digest and how many digests it computed. The answer is then a proof and a benchmark at once: how many hashes this browser did in a known time, on as many threads as it claimed to have.

What the hash rate reveals

The number of workers comes from the session's own claim. A session that reports 16 logical cores starts 12 workers; one that reports 4 starts 3. If those cores are really free, the total grows with the worker count. If they are not, the workers compete for the same cores and the total stays flat while the count grows. The same widget also times four WebAssembly arithmetic kernels in a separate worker, which gives a single-thread speed for comparison: total hashes divided by workers should match what that speed predicts.

Neither check needs a reference database; both compare the session with itself, which is the same idea as cross-API coherence checking. Typical contradictions:

  • Dense hosts. A server running dozens of browsers presents each with a desktop core count, but each gets a fraction of the CPU.
  • Containers with CPU limits. A container limited to two CPUs may still report the host's 64 cores, or a persona's 8.
  • Busy virtual machines. CPU taken by neighbouring guests shows up as uneven counts between workers of one proof.
  • Throttled background tabs. Hidden pages run timers and workers slower; the widget records visibility first.

What follows for automation

The claimed navigator.hardwareConcurrency and the CPU a session actually gets have to be chosen together. A browser on a shared host should claim a core count the host can actually give it, and the number of browsers per machine becomes a fingerprint decision as well as a cost one. The budget is measured with performance.now() inside the worker, so the clock also has to behave like the claimed browser's; see clock coherence detection.

Most fingerprint values are things the browser says. A proof of work is something the browser does, and the result is bounded by physics, so no JavaScript patch can report a faster hash rate than the machine delivers. A managed web data API handles this by matching the claimed hardware to the CPU each browser really gets, rather than by changing what the page reads.

Code example

javascript
// Time-bounded proof of work: count SHA-256 hashes per worker for 500 ms.
// Run in the browser console. Compare total hashes with the claimed cores.
const claimed = navigator.hardwareConcurrency;
const workers = Math.min(32, Math.max(1, Math.ceil(claimed * 0.75)));
const BUDGET_MS = 500;

const src = `onmessage = async ({ data: { budget, seed } }) => {
  const enc = new TextEncoder();
  const t0 = performance.now();
  let n = 0;
  while (performance.now() - t0 < budget) {
    await crypto.subtle.digest('SHA-256', enc.encode(seed + ':' + n));
    n++;
  }
  postMessage(n);
};`;
const url = URL.createObjectURL(new Blob([src], { type: 'text/javascript' }));

const counts = await Promise.all(Array.from({ length: workers }, (_, i) =>
  new Promise(resolve => {
    const w = new Worker(url);
    w.onmessage = e => { resolve(e.data); w.terminate(); };
    w.postMessage({ budget: BUDGET_MS, seed: 'demo-' + i });
  })));

const total = counts.reduce((a, b) => a + b, 0);
console.log({ claimed, workers, counts, total, perWorker: Math.round(total / workers) });
// On a real machine, perWorker stays roughly constant as workers grows.
// On a starved host, total stays flat and perWorker falls.

Related terms

Concept map

Concept map

How Proof-of-Work Challenge connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Does a proof-of-work challenge stop scrapers?

Not by itself. It makes each session cost CPU time, which matters at very high volume. A scraper running a real browser computes the proof like any visitor; the newer use is to compare the measured hash rate with the hardware the session claims.

Why does the number of CPU cores matter for a proof of work?

Because some challenges start one worker per claimed core and time them. If a session claims 16 cores but shares two physical cores with other browsers, the total hash count stays at the level of two cores, which no real 16-core machine produces.

Can the hash rate be faked from JavaScript?

Not in a way that survives checking. The digest has to be correct, so the work has to be done; reporting a higher count than was computed makes the best digest implausibly weak for the claimed number of attempts. The reliable approach is to give each browser the CPU its claim implies.

Last updated: 2026-10-28 · Facts last verified: 2026-10-28