Proof‑of‑work bot remediation at the edge. When Darwinium suspects automated abuse, it asks the visitor's browser to solve a small computational puzzle before the request is allowed through. Genuine users barely notice the delay; scripts, bots and headless browsers pay a heavy, compounding cost on every request.
Speedbump is a remediation action: you decide when it fires using your journey's rules and risk signals, and Darwinium handles issuing the challenge, solving it in the browser, and verifying the answer — transparently to your application.
Table of contents
- What Speedbump is (and why proof‑of‑work)
- How it works
- The challenge lifecycle
- Difficulty and repetitions
- Signals and attributes
- Implementing Speedbump
- Tuning guidance
- Security properties
- Limitations and operational notes
- Glossary
1. What Speedbump is (and why proof‑of‑work)
A Digital Speedbump is a challenge that costs the client CPU time rather than user attention. Instead of asking a human to click traffic lights, Darwinium sends a small program to the browser and requires it to find a numeric answer that can only be discovered by brute‑force computation. The browser repeats that work a configurable number of times before the original request is replayed to your application.
Why proof‑of‑work instead of a CAPTCHA?
| Speedbump (proof‑of‑work) | Traditional CAPTCHA | |
|---|---|---|
| User friction | Invisible — runs silently in JavaScript | High — the user must solve a puzzle |
| Attacker cost | Scales per request and compounds with repetitions | Cheap to farm out to human solving services |
| Accessibility | No user interaction, no images or audio | Well‑known accessibility problems |
| Bot resistance | Headless browsers cannot solve the challenge | Increasingly defeated by machine learning |
The key differentiator — a unique challenge every time.
Speedbump deliberately does not rely on standard, off‑the‑shelf algorithms for the puzzle. Well‑known algorithms are hardware‑accelerated, which would let an attacker's server farm dramatically out‑compute a real user's phone and let attackers reuse pre‑built solvers. Instead:
Every challenge uses its own freshly‑generated puzzle function. There is no faster way to solve it than to run the exact code Darwinium served — which puts a low‑power mobile browser and an attacker's server on a far more level footing, and makes automated tooling far less effective.
2. How it works
Speedbump involves three participants: the visitor's browser (running the Darwinium profiling script), the Darwinium edge, and your application. Darwinium keeps a small record of each outstanding challenge so it can verify the answer and bind it to the device it was issued to.

- A request arrives at a protected step. Your journey's rules assess risk.
- If the request looks automated, Darwinium generates a unique challenge (choosing a difficulty and a number of repetitions) and returns it to the browser using a dedicated activation HTTP status code.
- The Darwinium profiling script already running on the page intercepts that response automatically, solves the puzzle, and retries the original request with the solutions attached.
- Darwinium verifies every solution. If the challenge is complete, the request is forwarded to your application; otherwise the visitor is challenged again or blocked, according to your policy.
Because the browser's fetch/XHR calls are hooked by the profiling script, your application code never sees the challenge exchange — it only ever receives the final, real response.
3. The challenge lifecycle
A single Speedbump interaction can require several rounds. Each round the browser solves the required repetitions; Darwinium tracks how many repetitions remain across the whole interaction. While repetitions remain, the visitor is partially passed and issued the next challenge; once the budget is exhausted, the visitor has passed.

The lifecycle states, which you can branch on in your rules:

| Outcome | Meaning |
|---|---|
| Passed | All required repetitions solved. The request is forwarded to your application. |
| Partially passed | This round's solutions were valid, but more repetitions are required. A new challenge is issued and the browser loops. |
| Failed | Solutions were invalid, or were submitted from a different device than the challenge was issued to. |
| Replay | The same challenge was submitted more than once. |
4. Difficulty and repetitions
Two parameters control how much work the browser must do. You set them from your rules, typically scaled to the assessed risk of the request.
- Difficulty controls how hard each individual puzzle is to solve. Difficulty is exponential — each step up roughly doubles the expected work per solution. A difficulty of
0is a special value that passes automatically (useful for testing the wiring without imposing any cost). - Repetition is how many distinct solutions the browser must find and submit for a round. Repetitions are linear — doubling the count doubles the work, but keeps the variance of the solve time low, so legitimate users experience a predictable delay.
Rule of thumb: prefer more repetitions of a moderate difficulty over a single very hard puzzle. You get the same total attacker cost with a much more predictable delay for real users. Total client cost is approximately
repetition × 2^difficultypuzzle evaluations.
Darwinium also tracks remaining repetitions across rounds. This lets you demand a large total amount of work (for very suspicious traffic) while delivering it as a sequence of smaller, bounded challenges rather than one punishing puzzle.
5. Signals and attributes
Device signals
After verifying a submission, Darwinium emits one of the following device signals. Reference them in rules and strategies with has(profiling.device.signals, '<SIGNAL>'):
| Signal | Meaning |
|---|---|
SPEEDBUMP_CHALLENGE_PASSED |
All required repetitions solved; the visitor may proceed. |
SPEEDBUMP_CHALLENGE_PARTIALLY_PASSED |
Valid so far, but more repetitions are required (a new challenge is issued). |
SPEEDBUMP_CHALLENGE_FAILED |
Solutions invalid, or submitted from a different device/network than issued. |
SPEEDBUMP_CHALLENGE_INVALID |
The challenge state was malformed or could not be verified. |
SPEEDBUMP_CHALLENGE_EXPIRED |
The challenge is too old. |
SPEEDBUMP_CHALLENGE_REPLAY |
The same challenge was submitted more than once. |
Attributes
Speedbump populates the profiling.speedbump.* attribute family (attribute group Device). These are outputs — set by the platform and available in rules, strategies and investigations.
| Attribute | Type | Description |
|---|---|---|
profiling.speedbump.outgoing_challenge |
UUID | The challenge sent to the browser. Reference this in the challenge response body. |
profiling.speedbump.challenge |
UUID | The challenge identifier as received back from the browser. |
profiling.speedbump.difficulty |
Integer | Difficulty of the challenge (higher = exponentially harder). |
profiling.speedbump.repetition |
Integer | Number of distinct solutions required this round. |
profiling.speedbump.remaining_repetitions |
Integer | Remaining repetition budget across rounds. |
profiling.speedbump.solutions |
String array | The solutions submitted by the browser. |
profiling.speedbump.duration |
Integer | Time the browser took to solve, in microseconds. |
profiling.speedbump.start_time |
Date/Time | When the browser began solving. |
profiling.speedbump.replay_count |
Integer | How many times this challenge has been submitted. |
profiling.speedbump.seed |
Integer | Seed used to generate the challenge (also usable as a velocity key). |
The seed is a useful velocity key: a single seed appearing across a very large number of events is a strong indicator of automated re‑use, and can feed watch‑listing / block‑listing logic that escalates difficulty for repeat offenders.
6. Implementing Speedbump
A working Speedbump is assembled from six pieces across your journey. The diagram below shows what each piece is, which file it lives in, and how they hand off to one another at runtime — the browser (cyan) runs the solver, and everything else (indigo) is what you author in your journey.

| # | Piece | File | Job |
|---|---|---|---|
| 1 | Profiling injection | profiling step in .journey.yaml (snippet options) |
Turn on Speedbump so the solver ships with the page. |
| 2 | Protected journey step | .journey.yaml |
Attach the proxy action and challenge response to the URL you're protecting. |
| 3 | Decision rules | .rules |
Decide when to challenge — set a flag for suspicious requests. |
| 4 | Action rules | .rules (proxyAction) |
Set difficulty/repetitions per risk tier and return the activation code. |
| 5 | Challenge template | .template |
Render the challenge JSON body sent to the browser. |
| 6 | Strategy | .strategy |
Final gate — reject if a required challenge wasn't passed. |
The examples below are generic — adapt the step names, URLs and thresholds to your own journey.
6.1 Enable the profiling option
On the Inject Darwinium Profiling snippet, turn on Speedbump and choose the activation HTTP status code. This is the status code the edge uses to hand a challenge to the browser; the profiling script watches for exactly this code and solves the puzzle transparently.
# arguments passed to the inject_dwn_profiling template on a profiling step
- name: speedbump_enable
value: true
- name: speedbump_http_code
value: 418 # pick a code your origin never legitimately returns
Choose an activation code your application does not return on the protected endpoints (the platform default is
418, “I'm a teapot”). If your origin already uses that code for real responses, the solver could misfire — pick another unused code such as418or206.
6.2 Add the challenge response to a journey step
On the protected step, add a proxy action that returns the challenge body when your rules ask for a Speedbump. The response body is a small JSON document containing the challenge, its difficulty and the required repetitions.
Journey step (.journey.yaml):
- step_name: Protected API
proxy_event:
url: /api/protected
method: POST
request:
header_rules:
- name: dwn-profiling
extract_to_attribute: custom.general_purpose['profileBlob']
dependencies:
- Protected API.speedbumpDecision
proxy_action:
name: edgeChallenge
execute_code: src/speedbumpAction.rules
response_headers:
- name: Content-Type
content: application/json
# required for cross-origin API calls, or the browser will block the retry
- name: Access-Control-Allow-Origin
from_attribute: custom.general_purpose['origin']
condition: custom.general_purpose['speedbump'] = 'SPEEDBUMP'
response_body:
template_from_file: src/speedbump.template
event_type: api_request
models:
- name: speedbumpDecision
execute_code: src/speedbumpDecision.rules
Challenge response template (src/speedbump.template):
{% if custom.general_purpose['speedbump'] %}{"data": "{{ profiling.speedbump.outgoing_challenge }}", "difficulty": {{ profiling.speedbump.difficulty }}, "repetition": {{ profiling.speedbump.repetition }}}{% endif %}
datais the challenge the browser solves against (outgoing_challenge).difficultyandrepetitionare read from the attributes your rules set (see §6.4).
6.3 Decide when to challenge (rules)
The decision logic follows a simple precedence: let trusted clients straight through, hard‑block known‑bad ones, and reserve the challenge for everything in between — escalating difficulty with risk.

In a .rules file, set a flag when a request should be speed‑bumped. Only challenge traffic that isn't trusted and hasn't already passed. A minimal decision looks like this:
# src/speedbumpDecision.rules
version: 2
decision_type: scoringModel
rules:
# Trusted clients skip Speedbump entirely
- type: condition
condition: checkLabel("allow_list", [profiling.tcp_connection['PRIMARY'].ja4])
signal: speedbumpAllowListed
remediations:
- type: setAttribute
attribute: custom.general_purpose['skipSpeedbump']
value: "'true'"
# Challenge suspicious traffic that hasn't already passed
- type: condition
condition: >
custom.general_purpose['skipSpeedbump'] = null
and (!has(profiling.device.signals, 'SPEEDBUMP_CHALLENGE_PASSED')
or has(profiling.device.signals, 'SPEEDBUMP_CHALLENGE_PARTIALLY_PASSED'))
signal: SPEEDBUMP
category: Bot
remediations:
- type: setAttribute
attribute: custom.general_purpose['speedbump']
value: "'SPEEDBUMP'"
You can gate the challenge on any risk signal available in your journey — a model score, a bot signal, IP/JA4 velocity, or a seed appearing on a watch‑list. The pattern is always: set custom.general_purpose['speedbump'] = 'SPEEDBUMP' when you want a challenge issued.
6.4 Set difficulty and repetitions per risk tier (rules)
Escalate the cost with risk by setting profiling.speedbump.difficulty and profiling.speedbump.repetition (and, for multi‑round interactions, remaining_repetitions). This is typically done in the proxy‑action rules file that produces the challenge response:
# src/speedbumpAction.rules
version: 2
decision_type: proxyAction
rules:
# High risk: harder puzzle, larger total budget
- type: condition
condition: custom.general_purpose['speedbump'] = 'SPEEDBUMP' and outcome[CHAMPION].score < -300
signal: SPEEDBUMP_HIGH
remediations:
- type: setAttribute
attribute: profiling.speedbump.difficulty
value: "14"
- type: setAttribute
attribute: profiling.speedbump.repetition
value: "10"
- type: setAttribute
attribute: profiling.speedbump.remaining_repetitions
value: "30"
- type: proxyTerminate
outcome: HTTPReturn
error_or_redirect_code: 418 # must match speedbump_http_code
# Elevated risk: standard puzzle
- type: condition
condition: custom.general_purpose['speedbump'] = 'SPEEDBUMP'
signal: SPEEDBUMP_STANDARD
remediations:
- type: setAttribute
attribute: profiling.speedbump.difficulty
value: "12"
- type: setAttribute
attribute: profiling.speedbump.repetition
value: "10"
- type: proxyTerminate
outcome: HTTPReturn
error_or_redirect_code: 418
- The
proxyTerminate→HTTPReturnwith the activation code is what actually returns the challenge body (from the template in §6.2) to the browser. - On a partially passed round, re‑issue difficulty and repetition without resetting
remaining_repetitions, so the budget counts down rather than restarting.
6.5 Enforce the outcome (strategy)
A .strategy file runs last and produces the overall pass/reject decision. Use it as a backstop: if a request reached a protected step and did not pass its Speedbump, reject it.
# src/protected.strategy
version: 2
decision_type: strategy
rules:
- type: condition
condition: >
custom.general_purpose['speedbump'] = 'SPEEDBUMP'
and !has(profiling.device.signals, 'SPEEDBUMP_CHALLENGE_PASSED')
signal: speedbumpNotPassed
score: -1000
remediations:
- type: proxyTerminate
outcome: HTTPReturn
error_or_redirect_code: 200
7. Tuning guidance
- Start low, escalate on evidence. Begin with a modest difficulty for elevated risk and reserve the highest tiers for traffic that is already strongly suspected (velocity, watch‑listed seeds, low model scores). Let repeat offenders escalate themselves.
- Difficulty is exponential; repetition is linear. Raising difficulty by one step doubles per‑solution cost; raising repetition scales cost linearly while keeping the user‑visible delay predictable. Prefer repetitions for tuning the delay, difficulty for raising the ceiling.
- Benchmark on real mobile hardware. Validate solve times on a mid‑range phone before raising difficulty. A good target for a base tier is a barely‑perceptible delay (a few hundred milliseconds) on a typical device.
- Pick a safe activation code. Use an HTTP status your protected endpoints never legitimately return, so the solver can't trigger on a genuine response.
- Mind CORS. For cross‑origin API calls, the challenge response must include an
Access-Control-Allow-Originheader (as shown in §6.2) or the browser will block the automatic retry. - Use the seed as a velocity key. Aggregating on
profiling.speedbump.seedlets you detect and watch‑list challenge re‑use, feeding a self‑escalating loop against farming.
8. Security properties
Speedbump is designed to resist the ways attackers try to cheat proof‑of‑work:
- Hardware acceleration (ASIC/GPU/native solvers) is neutralised by generating a unique puzzle function per challenge — there is no pre‑built fast path, so the served code is the fastest route for everyone.
- Challenge farming (requesting challenges until an easy one appears) is countered by choosing difficulty from your risk assessment, and by watch‑listing seeds that are re‑used at volume.
- Solution replay is blocked on several fronts: each challenge is bound to the device and network it was issued to, submissions are counted (a second submission of the same challenge is flagged as a replay), and the device's secure identity nonce is refreshed on every solve so a copied submission fails independently.
- Headless automation generally cannot execute the injected challenge correctly, so it never produces valid solutions.
The visitor's browser is untrusted: nothing about a submitted solution is trusted until Darwinium independently re‑verifies it on the edge. Security rests on the cost of solving, the device/network binding, and replay counting — not on keeping the challenge secret.
9. Limitations and operational notes
- Requires the profiling script to run. Speedbump is a browser‑JavaScript mechanism. Pure API clients and legitimate non‑browser integrations cannot solve it — route those through your allow‑list (checked before any challenge) rather than speed‑bumping them.
- Edge platform. Speedbump verification runs on the Darwinium edge with challenge state persisted at the edge. Confirm availability for your specific edge deployment with your Darwinium contact before relying on it.
- Mobile WebViews. Validate behaviour in any embedded WebView contexts your users rely on, as part of rollout testing.
- Roll out behind a risk gate. Enable Speedbump for a targeted, suspicious slice of traffic first (via your rules) and widen once you've confirmed solve times and pass rates for legitimate users.
10. Glossary
- Proof‑of‑work (PoW): a puzzle that is expensive to solve but cheap to verify.
- Challenge (
data): the value the browser must solve against, referenced asprofiling.speedbump.outgoing_challenge. - Difficulty: how hard each puzzle is (exponential cost).
- Repetition: how many distinct solutions are required per round (linear cost).
- Remaining repetitions: the total work budget across multiple rounds; drives the partially passed → passed ladder.
- Activation status code: the HTTP status the edge uses to hand a challenge to the browser (
speedbump_http_code); the profiling script watches for it. - Seed: the value used to generate a challenge; also a useful velocity key for detecting re‑use.
- JA4: a TLS/HTTP client fingerprint commonly used for allow‑listing and block‑listing at the edge.