Cloudflare Workers as Your AI Backend: Zero-Server Architecture for Production AI
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:
- Static HTML/CSS/JS deployed to Cloudflare Pages
- Pages Functions (Workers running at
/api/*) handle dynamic logic - KV storage for rate limiting, lead storage, session state, and caching
- External API calls to LLM providers, email services, notification systems
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:
- ntfy push notification — self-hosted ntfy server (beamvideos.com:2586) with ntfy.sh as fallback. Delivers to phone within seconds.
- 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.
- 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:
- Cloudflare Pages: Free tier (unlimited sites, unlimited bandwidth, unlimited builds)
- Workers: Free tier (100,000 requests/day — far more than any small business needs)
- KV: Free tier (100K reads, 1K writes/day — the write limit is the only real constraint)
- Custom domains: Managed by Cloudflare DNS (free)
- SSL: Automatic, free, managed by Cloudflare
- CDN: Included, global, free
- DDoS protection: Included, free
- Total infrastructure: $0/month
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:
- Long-running tasks: Workers time out at 30 seconds on the paid plan, 10ms CPU time on free. Anything that takes longer needs a queue, a Durable Object, or an external server.
- Relational data: KV is key-value only. If you need joins, aggregations, or complex queries, look at Cloudflare D1 (SQLite at the edge) or an external database.
- WebSocket connections: Durable Objects handle persistent connections, but it is a fundamentally different programming model from standard request/response Workers.
- Large file storage: KV values max at 25MB. For images, videos, or large uploads, Cloudflare R2 (S3-compatible object storage) is the right tool.
- KV write limits: 1,000 writes/day on free. A high-traffic lead form might hit this. Paid Workers ($5/month) gives 1M writes/day.
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
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.
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