. Or use the REST API directly with POST /v1/verify."}},{"@type":"Question","name":"Is there a free tier?","acceptedAnswer":{"@type":"Answer","text":"Yes. Get an API key instantly with no signup required. The free tier includes 1,000 verifications per day (30,000/month). No credit card needed."}},{"@type":"Question","name":"What signals does Device.AI analyze?","acceptedAnswer":{"@type":"Answer","text":"Device.AI analyzes user agent, automation framework presence (Selenium, Puppeteer, PhantomJS, etc.), canvas fingerprint, WebGL renderer, screen resolution, timezone, hardware concurrency, browser feature support, and connection type."}}]}]}
← Back to Blog
Cloudflare WorkersBot DetectionEdge ComputingAPI

Bot Detection in Cloudflare Workers: Real-Time Protection at the Edge

·9 min read·Device.AI Engineering

Bot traffic hits your origin before traditional server-side detection can stop it. By the time your API route validates a request, the connection is established, bandwidth is consumed, and your rate limiters are already tracking the abuse. Cloudflare Workers offers a better approach: verify bot status at the edge, in milliseconds, before the request ever reaches your origin.

This guide walks through deploying bot detection to Cloudflare Workers using Device.AI, with complete working examples for common patterns.

Why Edge Bot Detection Matters

Latency

Cloudflare Workers run on servers within milliseconds of users globally. A bot-detection API call to device.ai from a Cloudflare Worker completes in 5-20ms (not 100-200ms from your distant origin server). For a visitor in Tokyo hitting your US origin, this saves 100-150ms of latency before the request is even validated.

Origin Protection

Malicious traffic never reaches your origin. That means:

  • Your rate limiters don't get congested with bot traffic
  • Your database isn't queried for verification of bots
  • Your infrastructure costs don't spike during attacks
  • Your logs stay clean — bot requests don't pollute your analytics

Scale Without Cost

Cloudflare Workers scale horizontally across 300+ data centers. Processing millions of bot-detection requests per day from the edge costs significantly less than processing them on your origin infrastructure.

Architecture: How It Works

┌──────────────────────────────────────────────────────┐
│                    Visitor Request                   │
└──────────────────────────────────┬──────────────────┘
                     │
                     v
          ┌──────────────────────┐
          │ Cloudflare Workers   │
          │ (Bot Detection)      │
          └──────────────────────┘
                     │
          ┌──────────────────┴──────────────┐
          │ Call Device.AI API              │
          │ Score: 0.15 (bot)              │
          v                                 v
      ✗ BLOCK                           ✓ ALLOW
      403 Forbidden                       │
      (never hits origin)                 v
                            ┌──────────────────────┐
                            │  Your Origin         │
                            │  (App Server)        │
                            └──────────────────────┘

Step 1: Create Your Worker

// wrangler.toml
name = "bot-detection-worker"
main = "src/index.ts"
compatibility_date = "2026-07-01"

[env.production]
route = "example.com/*"
zone_id = "YOUR_ZONE_ID"

[[env.production.env]]
vars = { DEVICE_AI_KEY = "dv_live_YOUR_KEY" }

Step 2: Write the Bot Detection Handler

// src/index.ts
export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);

    // Get visitor's IP and User-Agent
    const ip = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
    const userAgent = request.headers.get('User-Agent') || '';

    // Call Device.AI to verify
    const verifyRes = await fetch('https://device.ai/v1/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        key: env.DEVICE_AI_KEY,
        userAgent,
        ip,
      }),
    });

    const verification = await verifyRes.json() as {
      score: number;
      bot: boolean;
      risk: string;
    };

    // Block obvious bots
    if (verification.score < 0.3 || verification.bot) {
      return new Response('Access Denied', { status: 403 });
    }

    // Continue to origin for legitimate traffic
    return fetch(request);
  },
};

interface Env {
  DEVICE_AI_KEY: string;
}

Step 3: Deploy

npx wrangler deploy --env production

That's it. Your Worker is now live, verifying every request at the edge.

Pattern 1: Route-Specific Protection

Not every route needs the same protection level. API endpoints and form submissions are high-risk. Public content (blog posts, marketing pages) can be more permissive.

// src/index.ts
export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    const ip = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
    const userAgent = request.headers.get('User-Agent') || '';

    // High-risk routes: strict verification
    const strictRoutes = ['/api/', '/checkout/', '/login/', '/signup/'];
    const threshold = strictRoutes.some(r => url.pathname.startsWith(r))
      ? 0.3  // Strict: block any suspicious score
      : 0.1; // Permissive: only block obvious bots

    const verification = await (await fetch('https://device.ai/v1/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: env.DEVICE_AI_KEY, userAgent, ip }),
    })).json() as { score: number; bot: boolean };

    if (verification.score < threshold) {
      return new Response('Access Denied', { status: 403 });
    }

    return fetch(request);
  },
};

interface Env {
  DEVICE_AI_KEY: string;
}

Pattern 2: Graceful Fallback (Circuit Breaker)

Device.AI is highly available, but the internet is unpredictable. If the verification API times out or fails, you have three options: fail open (allow the request), fail closed (block it), or allow but log for manual review. Here's a fail-open approach:

// src/index.ts
export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    const ip = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
    const userAgent = request.headers.get('User-Agent') || '';

    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 2000); // 2s timeout

      const verifyRes = await fetch('https://device.ai/v1/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ key: env.DEVICE_AI_KEY, userAgent, ip }),
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      const verification = await verifyRes.json() as { score: number; bot: boolean };

      if (verification.score < 0.3 || verification.bot) {
        // Log the fraudulent click
        await env.BLOCKED_IPS.put(
          `${Date.now()}_${ip}`,
          JSON.stringify({ ip, userAgent, score: verification.score })
        );
        return new Response('Access Denied', { status: 403 });
      }
    } catch (error) {
      // Verification API failed. Log and allow (fail-open).
      console.error('Device.AI verification failed:', error);
      await env.FAILED_VERIFICATIONS.put(
        `${Date.now()}_${ip}`,
        `Verification failed: ${error}`
      );
      // Continue to origin anyway
    }

    return fetch(request);
  },
};

interface Env {
  DEVICE_AI_KEY: string;
  BLOCKED_IPS: R2Bucket;
  FAILED_VERIFICATIONS: KVNamespace;
}

Pattern 3: Adaptive Scoring by Request Type

Different request types have different false-positive costs. A rejected login attempt is annoying; a rejected form submission is a lost lead. Adjust thresholds dynamically:

// src/index.ts
const requestTypeThresholds = {
  'api/payments': 0.5,     // Payments: strict (false positives are rare)
  'api/login': 0.4,        // Login: strict (can use password reset)
  'api/contact': 0.2,      // Forms: permissive (cost of FP is high)
  'public/': 0.05,         // Public pages: almost never block
};

function getThreshold(pathname: string): number {
  for (const [pattern, threshold] of Object.entries(requestTypeThresholds)) {
    if (pathname.startsWith(pattern)) return threshold;
  }
  return 0.3; // default
}

export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    const ip = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
    const userAgent = request.headers.get('User-Agent') || '';
    const threshold = getThreshold(url.pathname);

    const verification = await (await fetch('https://device.ai/v1/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: env.DEVICE_AI_KEY, userAgent, ip }),
    })).json() as { score: number; bot: boolean };

    if (verification.score < threshold) {
      return new Response('Access Denied', { status: 403 });
    }

    return fetch(request);
  },
};

interface Env {
  DEVICE_AI_KEY: string;
}

Pattern 4: Caching for Cost Optimization

The same user agent from the same IP range likely has the same bot status for hours. Cache the result in Cloudflare KV to save API calls on high-traffic sites:

// src/index.ts
export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    const ip = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
    const userAgent = request.headers.get('User-Agent') || '';
    const cacheKey = `device:${ip}:${userAgent}`;

    // Check KV cache first
    let verification = await env.CACHE.get(cacheKey).then(v => v ? JSON.parse(v) : null);

    if (!verification) {
      // Cache miss — call Device.AI
      const res = await fetch('https://device.ai/v1/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ key: env.DEVICE_AI_KEY, userAgent, ip }),
      });
      verification = await res.json();

      // Cache for 6 hours (21600 seconds)
      await env.CACHE.put(cacheKey, JSON.stringify(verification), { expirationTtl: 21600 });
    }

    if (verification.score < 0.3 || verification.bot) {
      return new Response('Access Denied', { status: 403 });
    }

    return fetch(request);
  },
};

interface Env {
  DEVICE_AI_KEY: string;
  CACHE: KVNamespace;
}

Pattern 5: Rate Limiting Based on Risk Score

Instead of simply blocking bots, rate-limit them based on their risk score. Legitimate users get high limits; suspicious traffic gets strict limits:

// src/index.ts
export default {
  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url);
    const ip = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
    const userAgent = request.headers.get('User-Agent') || '';

    const verification = await (await fetch('https://device.ai/v1/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: env.DEVICE_AI_KEY, userAgent, ip }),
    })).json() as { score: number };

    // Determine rate limit tier based on score
    const tier = verification.score > 0.7
      ? { requests: 1000, window: 60 }  // Clean traffic
      : verification.score > 0.3
      ? { requests: 100, window: 60 }   // Suspicious
      : { requests: 5, window: 60 };    // Likely bot

    // Apply rate limiting (simplified — use Durable Objects in production)
    const countKey = `rl:${ip}`;
    const count = await env.KV.get(countKey).then(v => parseInt(v || '0') + 1);
    await env.KV.put(countKey, count.toString(), { expirationTtl: tier.window });

    if (count > tier.requests) {
      return new Response('Rate Limited', { status: 429 });
    }

    return fetch(request);
  },
};

interface Env {
  DEVICE_AI_KEY: string;
  KV: KVNamespace;
}

Monitoring and Alerting

Log all bot blocks to R2 and set up Cloudflare Logpush to monitor trends:

// Send logs to R2
const blocked = {
  timestamp: Date.now(),
  ip,
  userAgent,
  score: verification.score,
  path: url.pathname,
};

await env.LOGS.put(
  `blocked/${new Date().toISOString().split('T')[0]}/${ip}_${Date.now()}`,
  JSON.stringify(blocked)
);

Then query trends with SQL on R2:

-- SQL query on R2
SELECT
  DATE(timestamp) as day,
  COUNT(*) as blocks,
  COUNT(DISTINCT ip) as unique_ips,
  AVG(score) as avg_score
FROM s3_object_lambda_table
WHERE path LIKE 'blocked/%'
GROUP BY DATE(timestamp)
ORDER BY day DESC;

Cost Analysis

Cloudflare Workers include 100K free requests/day. Device.AI's free tier includes 1,000 verifications/day. For a site with 50K requests/day and 5K unique visitors:

  • Cloudflare cost: $0 (under free tier)
  • Device.AI cost: ~500 verifications (under free tier if unique visitors + margin)
  • Total: $0 for both

Once you exceed free tiers:

  • Cloudflare: $0.15 per 100K requests (Bundled Plan) or $0.30 (Pay-as-you-go)
  • Device.AI: $0.001 per verification (generous overage pricing)

For 1M verifications/month, that's $1,000/month in Workers cost + $1,000/month in Device.AI cost = $2,000/month to protect your entire infrastructure. Most sites will fall well below this.

Getting Started

  1. Get a Device.AI key: Visit device.ai and click "Get Free API Key" (no signup required)
  2. Install Wrangler: `npm install -g wrangler`
  3. Create a Worker: `wrangler init my-bot-detection`
  4. Copy one of the patterns above into `src/index.ts`
  5. Add your Device.AI key: `wrangler secret put DEVICE_AI_KEY`
  6. Deploy: `wrangler deploy`

You now have bot detection running at the Cloudflare edge, protecting your entire origin from malicious traffic — with <1ms detection latency and zero cost for the first 100K requests per day.

Ready to stop bots?

Get a free API key instantly. No signup, no credit card.

Get Free API Key →