CloudflareArchitectureEdge Computing

Cloudflare Workers as Your AI Backend: Zero-Server Architecture for Production AI

Brandon Davis · September 11, 2026 · 11 min read

I run 10+ production websites with AI-powered backends. None of them have a traditional server. Every API endpoint, lead capture system, notification pipeline, and AI integration runs on Cloudflare Workers — serverless functions at the edge with KV storage.

This is not a toy setup. These systems handle real customers, process real form submissions, and send real notifications. The total infrastructure cost for all of them combined: $0/month. Here is the architecture, the patterns that work, and the hard lessons from deploying across a fleet.

The Pattern: Pages Functions + KV + External APIs

Every site follows the same architecture:

No EC2 instances. No Docker containers. No Kubernetes. No database server. The entire backend fits in a functions/ directory alongside the static site.

project-root/
├── index.html
├── about.html
├── contact.html
├── sitemap.xml
├── _headers          # Security headers + cache rules
├── wrangler.toml     # KV bindings + project config
└── functions/
    └── api/
        ├── contact.js    # Lead capture endpoint
        ├── chat.js       # AI chat endpoint
        └── health.js     # Monitoring endpoint

When you run wrangler pages deploy, Cloudflare compiles the functions/ directory into Workers that run at the corresponding URL paths. No build step, no bundler configuration, no Docker image to push. Drop JavaScript files in a folder and deploy.

The Lead Capture Pattern (Deployed Across 10+ Sites)

The most reusable pattern in my fleet is the lead capture endpoint. Every business site needs one, and the same architecture works whether it is a phone repair shop, an HVAC contractor, or an AI consulting portfolio. Here is the real code:

POST /api/contact (or /api/lead, /api/repair-lead, etc.)

export async function onRequestPost({ request, env }) {
  const body = await request.json();
  const { name, email, phone, message, service, _honey } = body;

  // 1. Honeypot check (bots fill hidden fields)
  if (_honey) return new Response(JSON.stringify({ ok: true }), {
    status: 200  // Pretend success to the bot
  });

  // 2. Validate required fields
  if (!name || !email) return new Response(
    JSON.stringify({ error: 'Name and email required' }),
    { status: 400 }
  );

  // 3. Rate limit (5 per hour per IP)
  const ip = request.headers.get('CF-Connecting-IP');
  try {
    const key = `ratelimit:${ip}`;
    const count = parseInt(await env.LEADS.get(key) || '0');
    if (count >= 5) return new Response(
      JSON.stringify({ error: 'Too many requests' }),
      { status: 429 }
    );
    await env.LEADS.put(key, String(count + 1),
      { expirationTtl: 3600 });
  } catch (e) {
    // KV quota exhausted — continue anyway
  }

  // 4. Store lead with 90-day TTL
  let stored = false;
  try {
    const id = crypto.randomUUID();
    await env.LEADS.put(`lead:${id}`, JSON.stringify({
      name, email, phone, message, service,
      ip, ts: new Date().toISOString()
    }), { expirationTtl: 7776000 });
    stored = true;
  } catch (e) { /* KV quota — lead still gets notified */ }

  // 5. Push notification via ntfy
  const notified = await notify(env, { name, email, message });

  // 6. SMS broadcast via email-to-SMS gateway
  const sms = await broadcastSMS(env, { name, email, phone });

  return new Response(JSON.stringify({
    ok: true, stored, notified, sms
  }));
}

This exact pattern — with customization for field names, notification topics, and SMS numbers — runs on PlexFix, Brandon AI, PJ's HVAC, I Haul Toledo, Marion Remote Fix, LC Repair, and more.

KV Storage: Patterns and Pitfalls

Cloudflare KV is eventually-consistent key-value storage with a generous free tier (100,000 reads and 1,000 writes per day). I use it for four distinct patterns:

Rate Limiting

ratelimit:{ip} keys with 1-hour TTL. Simple, effective, and does not require any external service. The key auto-expires, so there is no cleanup needed.

Lead Storage

lead:{uuid} keys with 90-day TTL. Each lead gets its own key with a UUID. For the leads dashboard, I also maintain a lead:recents index that tracks the last 50 lead IDs for quick retrieval without scanning.

Session State

For AI chat endpoints, chat:{sessionId} keys store the full conversation history as JSON. 24-hour TTL for active conversations. This is how the AI sales agent on LuxuriousComputers.com maintains conversation context across page refreshes.

Response Caching

Expensive API responses (eBay search results, product pricing data) get cached with short TTLs (5-15 minutes). A single eBay API call costs ~300ms; a KV read costs ~5ms. For high-traffic endpoints, caching turns a 300ms response into a 5ms response.

The KV Quota Gotcha (The Hard Way)

KV write operations are capped at 1,000/day on the free tier. When you hit the limit, writes throw error code 10048. This is where I learned a painful lesson:

If your rate limiter is inside a try block and KV throws an error, the rate limiter fails open — every request gets through. If your lead storage throws an error and your error handling crashes the entire request, the lead is lost. No notification, no storage, nothing.

The fix: wrap every KV write in a try/catch that allows the request to proceed even if storage fails. The lead still gets sent via ntfy and SMS — you just lose the KV record. I learned this when 3 of my 5 lead capture APIs were silently dropping leads during quota exhaustion. The leads were not stored, and the notification call after the KV call never fired because the error bubbled up and killed the handler.

// WRONG: KV failure kills the notification
await env.LEADS.put(key, data);   // throws on quota
await notify(env, data);          // never reached

// RIGHT: KV failure is isolated
let stored = false;
try {
  await env.LEADS.put(key, data);
  stored = true;
} catch (e) { /* quota exhausted — continue */ }
const notified = await notify(env, data);  // always fires

Notification Pipeline: Belt and Suspenders

Every lead triggers three notification channels in parallel using context.waitUntil so they do not block the response:

  1. ntfy push notification — self-hosted ntfy server (beamvideos.com:2586) with ntfy.sh as fallback. Delivers to phone within seconds.
  2. SMS via email-to-SMS gateway — broadcasts to 8 US carrier gateways (@vtext.com, @txt.att.net, @tmomail.net, etc.). Only the real carrier delivers; the rest bounce silently. No Twilio account needed.
  3. KV storage — durable record for the leads dashboard and audit trail.

If ntfy is down, SMS still fires. If the carrier gateway is wrong for 7 of 8 carriers, the right one still delivers. If KV quota is exhausted, the lead still reaches the phone. No single failure loses a lead.

Security Headers: The _headers File

Every site in the fleet uses a _headers file to set security headers at the CDN level:

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  X-XSS-Protection: 1; mode=block
  Strict-Transport-Security: max-age=31536000; includeSubDomains

/favicon.svg
  Cache-Control: public, max-age=31536000, immutable

/favicon.ico
  Cache-Control: public, max-age=31536000, immutable

These headers are applied by Cloudflare at the edge, before the response reaches the browser. No middleware, no framework configuration, no server-side code needed. Drop a file, deploy, done.

Deployment: The OAuth Token Dance

Deploying to Cloudflare Pages from an automated script requires OAuth token management. The wrangler CLI uses OAuth tokens that expire, and if you are deploying from a CI/CD pipeline or a scheduled task, you need automatic refresh.

I wrote a Python deploy script that handles this for every site in the fleet:

# Simplified deploy pattern
auth = read_wrangler_oauth()       # Read ~/.wrangler/config/default.toml
if is_token_expired(auth):
    result = refresh_oauth(auth['refresh_token'])
    save_wrangler_auth(result)
os.environ['CLOUDFLARE_API_TOKEN'] = auth['oauth_token']
os.environ['CLOUDFLARE_ACCOUNT_ID'] = 'fbb...'

subprocess.run('npx wrangler pages deploy . '
  '--project-name=my-site --branch=main', shell=True)

One gotcha: Cloudflare Pages requires CLOUDFLARE_ACCOUNT_ID set via environment variable, not via wrangler.toml, when you have multiple accounts. The wrangler CLI will silently deploy to the wrong account if you get this wrong.

Cost Breakdown: 10+ Sites for $0/Month

For 10+ production sites with active lead capture, AI chat, and notification pipelines:

The only costs are external: Claude API for AI chat (~$5-15/month depending on volume), domain registration (~$10/year per domain), and the self-hosted inference server (runs on existing homelab infrastructure).

When This Architecture Breaks Down

Workers are not suitable for everything. Know the limits:

For the vast majority of small business AI backends — lead capture, chat, notifications, caching, simple CRUD — Workers + KV on the free tier is more than enough.

Related Articles

AI SalesProduction

Why Your AI Chatbot Sucks (And How to Build One That Closes Sales)

The AI sales agent that runs on this Workers architecture — live inventory, cart awareness, session memory, and multi-model routing.

Self-HostedGPU

How I Run a 27B LLM on Consumer GPUs

The local inference server that complements the Workers stack — 20 tok/s on consumer hardware for batch and internal workloads.

Need an AI backend without the server hassle?

I build production AI systems on Cloudflare Workers — lead capture, AI chat, notification pipelines, and more. Zero infrastructure cost, zero servers to manage. The architecture described in this article is what I deploy for every client.

Get in touch