Skip to main content
Challenges are the core mechanism Anubis uses to distinguish humans from bots. When a request triggers a CHALLENGE action, Anubis issues a computational puzzle that is trivial for browsers but expensive for scrapers at scale.

Challenge Metadata

Every challenge issued by Anubis contains structured metadata:
// From lib/challenge/challenge.go:6
type Challenge struct {
    IssuedAt       time.Time         `json:"issuedAt"`
    Metadata       map[string]string `json:"metadata"`
    ID             string            `json:"id"`
    Method         string            `json:"method"`
    RandomData     string            `json:"randomData"`
    PolicyRuleHash string            `json:"policyRuleHash,omitempty"`
    Difficulty     int               `json:"difficulty,omitempty"`
    Spent          bool              `json:"spent"`
}
ID
string
required
UUID v7 identifier for the challenge. Used for lookups and validation.
Method
string
required
Challenge algorithm: fast or slow (deprecated).
RandomData
string
required
64 bytes of random data (hex-encoded) that the client must process.
Difficulty
int
default:"0"
Number of leading zero bits required in the proof-of-work hash (0-64).
PolicyRuleHash
string
Hash of the bot rule that issued this challenge. Used to invalidate tokens when policy changes.
Spent
bool
default:"false"
Whether this challenge has been solved. Prevents double-spend attacks.

Challenge Types

Anubis supports multiple challenge algorithms, registered in the challenge registry:
bots:
  - name: suspicious-traffic
    expression:
      - "req.headers['user-agent'].contains('bot')"
    action: CHALLENGE
    challenge:
      algorithm: fast
      difficulty: 3
The proof-of-work challenge requires clients to find a nonce such that:
SHA256(randomData + nonce) = hash with N leading zero bits
Where N is the configured difficulty.
The slow algorithm is deprecated. Use fast for all new deployments. The algorithms are functionally identical; the naming was legacy.

Meta Refresh

The metarefresh challenge uses HTTP meta refresh tags to redirect browsers. This is a passive challenge that doesn’t require JavaScript or computation.
Registered as metarefresh in the challenge registry. Useful for detecting scrapers that don’t process HTML meta tags.

Preact (Interactive)

The preact challenge renders an interactive UI component using Preact. This challenge type verifies JavaScript execution and DOM manipulation capabilities.

Proof-of-Work Mechanism

Validation Algorithm

When a client submits a solution, Anubis validates it in constant time:
// From lib/challenge/proofofwork/proofofwork.go:35
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
    // Extract parameters
    nonceStr := r.FormValue("nonce")
    elapsedTimeStr := r.FormValue("elapsedTime")
    response := r.FormValue("response")
    
    // Recompute hash
    calcString := fmt.Sprintf("%s%d", challenge, nonce)
    calculated := internal.SHA256sum(calcString)
    
    // Constant-time comparison prevents timing attacks
    if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
        return chall.ErrFailed
    }
    
    // Verify difficulty requirement
    if !strings.HasPrefix(response, strings.Repeat("0", difficulty)) {
        return chall.ErrFailed
    }
    
    return nil
}
1

Extract Parameters

The client submits nonce, elapsedTime, and response (the computed hash).
2

Recompute Hash

Server independently computes SHA256(randomData + nonce).
3

Constant-Time Comparison

Uses crypto/subtle.ConstantTimeCompare to prevent timing side-channels.
4

Verify Difficulty

Ensures the hash has the required number of leading zero hex characters.

Client-Side Solving

The challenge page includes JavaScript that brute-forces the nonce:
const randomData = "/* from server */";
const difficulty = 3;

let nonce = 0;
const start = performance.now();

while (true) {
  const input = randomData + nonce;
  const hash = await sha256(input);
  
  if (hash.startsWith('0'.repeat(difficulty))) {
    const elapsed = (performance.now() - start) / 1000;
    submitSolution(nonce, hash, elapsed);
    break;
  }
  
  nonce++;
}

Difficulty Settings

Difficulty determines how many leading zero hex characters (4 bits each) are required:
Difficulty Range
int
default:"0"
Valid range: 0-64
  • 0: No proof-of-work required (instant pass)
  • 1-3: Light verification (milliseconds)
  • 4-6: Moderate difficulty (seconds)
  • 7-10: Heavy computation (tens of seconds)
  • 11+: Extreme difficulty (minutes to hours)

Difficulty Performance Table

DifficultyExpected AttemptsAverage Time (Modern CPU)Use Case
01InstantTesting only
116less than 10msVery light verification
2256~50msLight bot deterrent
34,096~500msRecommended default
465,536~5sModerate protection
51,048,576~1minHeavy scraper deterrent
616,777,216~15minExtreme protection
These times are approximate and vary based on client hardware and JavaScript engine performance.

Configuring Difficulty

bots:
  - name: aggressive-crawler
    user_agent_regex: "(curl|wget|python-requests)"
    action: CHALLENGE
    challenge:
      algorithm: fast
      difficulty: 4  # Harder for known scrapers

  - name: suspicious-behavior
    expression:
      - "req.path.matches('/api/.*')"
    action: CHALLENGE
    challenge:
      algorithm: fast
      difficulty: 2  # Lighter for API endpoints

Challenge Lifecycle

Double-Spend Protection: Each challenge can only be solved once. The Spent flag prevents replay attacks where an attacker tries to reuse a valid solution.

Storage Requirements

Challenges are stored with a 30-minute TTL:
// From lib/anubis.go:133
if err := j.Set(ctx, "challenge:"+id.String(), chall, 30*time.Minute); err != nil {
    return nil, err
}
Storage backends must support:
  • JSON serialization: Challenges are stored as store.JSON[challenge.Challenge]
  • TTL/Expiration: Automatic cleanup after 30 minutes
  • Atomic updates: For marking challenges as spent
For high-traffic deployments, use Valkey/Redis or another distributed store to share challenge state across multiple Anubis instances.

Challenge Metrics

Anubis exposes Prometheus metrics for monitoring:
# Challenges issued by method
anubis_challenges_issued{method="embedded"} 1523
anubis_challenges_issued{method="api"} 42

# Challenges successfully validated
anubis_challenges_validated{method="fast"} 1401

# Failed validation attempts
anubis_failed_validations{method="fast"} 164

# Time taken to solve challenges (histogram)
anubis_challenge_time_taken_seconds{algorithm="fast"}

Error Handling

Challenge validation can fail with specific errors:
// From lib/challenge/error.go:9
var (
    ErrFailed        = errors.New("challenge: user failed challenge")
    ErrMissingField  = errors.New("challenge: missing field")
    ErrInvalidFormat = errors.New("challenge: field has invalid format")
)
The client didn’t submit required parameters (nonce, elapsedTime, or response).HTTP Status: 400 Bad Request
Parameters have wrong type (e.g., non-numeric nonce).HTTP Status: 400 Bad Request
The solution is incorrect or doesn’t meet difficulty requirements.HTTP Status: 403 Forbidden

Best Practices

Start Low

Begin with difficulty 2-3 and increase only if you observe scraper persistence.

Monitor Solve Times

Use anubis_challenge_time_taken_seconds to ensure challenges aren’t frustrating legitimate users.

Different Difficulty by Context

Use lower difficulty for public pages, higher for sensitive endpoints.

Test Cookie Support

Anubis automatically checks cookie support. Failed cookie tests appear in logs.

Custom Challenge Implementation

You can implement custom challenges by satisfying the challenge.Impl interface:
// From lib/challenge/interface.go:59
type Impl interface {
    // Setup registers any additional routes with the Impl for assets or API routes.
    Setup(mux *http.ServeMux)

    // Issue a new challenge to the user, called by the Anubis.
    Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)

    // Validate a challenge, making sure that it passes muster.
    Validate(r *http.Request, lg *slog.Logger, in *ValidateInput) error
}
Register your implementation:
func init() {
    challenge.Register("my-custom-challenge", &MyImpl{)
}

Next Steps

Policies

Learn how to configure bot detection rules that trigger challenges

How It Works

Understand the complete request flow and JWT validation

Build docs developers (and LLMs) love