All articles
Build Guides

Build an On-Call Incident Summarizer with Cloudflare Workers AI

FDE Coach EditorialAugust 5, 202611 min read

What You’re Building

A serverless endpoint that turns chaotic log dumps into structured incident narratives. You paste raw log lines—stack traces, error codes, timestamps—and the system returns a clean Markdown postmortem.

Feature list:

  • Accepts raw log text via HTTP POST
  • Calls Cloudflare Workers AI (free tier) with a structured prompt
  • Returns a Markdown postmortem with sections: Summary, Root Cause, Timeline, Impact, Action Items
  • Runs entirely within Cloudflare’s free tier limits (100k requests/day, 10ms CPU time per invocation is plenty for LLM calls that run under the 10s Workers AI timeout)

This is a pure engineering tool—no frontend, no database, no auth. Just logs in, Markdown out. If you’ve ever spent a bleary-eyed 3am on-call shift trying to reconstruct what happened from fragmented logs, this is your new best friend.

Architecture Overview

The flow is dead simple: an HTTP request hits your Worker, which constructs a carefully engineered prompt containing the raw logs and explicit formatting instructions. That prompt goes to the free Workers AI text generation model, which returns structured Markdown. The Worker sanitizes the output and sends it back.

No vector databases, no RAG, no state. The LLM does all the heavy lifting—and because Workers AI runs on Cloudflare’s global GPU infrastructure, inference latency is surprisingly low (typically 2-5 seconds for this use case).

Prerequisites (All Free Tier)

  1. Cloudflare account – Sign up at dash.cloudflare.com/sign-up. Free tier gives you 100k Worker requests/day and 10k Workers AI inferences/day.
  2. Node.js 18+ – Install from nodejs.org. We use npm to scaffold and deploy.
  3. Wrangler CLI – Cloudflare’s deployment tool. Install globally:
    npm install -g wrangler
    
    Then authenticate:
    wrangler login
    
  4. Workers AI enabled – Go to the Cloudflare Dashboard → Workers & Pages → AI and enable Workers AI for your account. This takes one click and costs nothing.

That’s it. No credit card needed, no API keys to manage. The LLM model we’ll use—@cf/meta/llama-3.1-8b-instruct—is included in the free tier with 10k daily inferences.

Step 1: Scaffold the Cloudflare Worker

Create a new project directory and initialize a Worker:

mkdir incident-summarizer
cd incident-summarizer
npm create cloudflare@latest -- incident-summarizer

When prompted, choose:

  • “Hello World” Worker as the template
  • TypeScript – yes, we want types
  • No to deploying immediately (we’ll deploy after writing code)

This generates a src/index.ts with a basic fetch handler. Open wrangler.toml and add the Workers AI binding:

name = "incident-summarizer"
main = "src/index.ts"
compatibility_date = "2024-12-01"

[ai]
binding = "AI"

The [ai] binding gives your Worker access to the AI object for inference calls. No environment variables, no secrets—Cloudflare handles auth transparently.

Step 2: Write the Incident Summarizer Logic

Replace the entire contents of src/index.ts with this:

export interface Env {
  AI: Ai;
}

interface RequestBody {
  logs: string;
  incidentTitle?: string;
}

const SYSTEM_PROMPT = `You are an expert SRE writing a postmortem. Analyze the provided logs and generate a structured incident report in Markdown.

Your response MUST follow this exact format:

## Summary
[2-3 sentence plain-English summary of what happened]

## Root Cause
[Technical root cause analysis based on log evidence]

## Timeline (UTC)
- [Timestamp from logs] — [Event description]
- [Timestamp from logs] — [Event description]
(Extract actual timestamps from the logs provided)

## Impact
[What users experienced, affected services, duration if discernible]

## Action Items
- [ ] [Immediate fix]
- [ ] [Long-term prevention]

Rules:
- Extract timestamps from the logs themselves. Do not invent times.
- If you cannot determine something, say "Insufficient log data to determine" rather than guessing.
- Be concise. Engineers read postmortems to learn, not to admire prose.`;

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Only accept POST requests
    if (request.method !== 'POST') {
      return new Response(
        JSON.stringify({ error: 'Send a POST request with {"logs": "..."}' }),
        { status: 405, headers: { 'Content-Type': 'application/json' } }
      );
    }

    let body: RequestBody;
    try {
      body = await request.json() as RequestBody;
    } catch {
      return new Response(
        JSON.stringify({ error: 'Invalid JSON body' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    if (!body.logs || typeof body.logs !== 'string' || body.logs.trim().length === 0) {
      return new Response(
        JSON.stringify({ error: 'Missing or empty "logs" field' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    const userPrompt = body.incidentTitle
      ? `Incident title: ${body.incidentTitle}\n\nLogs:\n${body.logs}`
      : `Logs:\n${body.logs}`;

    try {
      const response = await env.AI.run(
        '@cf/meta/llama-3.1-8b-instruct',
        {
          messages: [
            { role: 'system', content: SYSTEM_PROMPT },
            { role: 'user', content: userPrompt },
          ],
          max_tokens: 2048,
          temperature: 0.3, // Low temperature for factual consistency
        }
      );

      // @ts-expect-error Workers AI response shape
      const generatedText = response.response || response;

      return new Response(generatedText, {
        headers: {
          'Content-Type': 'text/markdown; charset=utf-8',
          'Access-Control-Allow-Origin': '*',
        },
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Unknown error';
      return new Response(
        JSON.stringify({ error: `AI inference failed: ${message}` }),
        { status: 500, headers: { 'Content-Type': 'application/json' } }
      );
    }
  },
};

Key design decisions in this code:

  • Temperature 0.3 – We want deterministic, factual output. Higher temperatures cause hallucinated timestamps and creative root cause analysis, which is the opposite of what you need in a postmortem.
  • Strict prompt engineering – The system prompt enforces a specific Markdown structure. Without this, the model tends to ramble or invent sections.
  • Explicit error handling – Each failure mode (wrong method, bad JSON, missing logs, AI failure) returns a distinct error. This makes the endpoint debugable from curl.
  • Access-Control-Allow-Origin: * – Lets you call this from browser-based tools if you ever build a frontend for it.

The @cf/meta/llama-3.1-8b-instruct model is chosen because it’s fast (sub-3-second responses for this prompt size), free, and follows formatting instructions reliably. For a deeper dive into prompt engineering patterns like this, check out our FDE weekly workflow guide which covers how forward deployed engineers structure prompts for production use.

Step 3: Deploy and Test with Real Logs

Deploy the Worker:

npx wrangler deploy

Wrangler outputs a URL like https://incident-summarizer.your-subdomain.workers.dev. Test it immediately:

curl -X POST https://incident-summarizer.your-subdomain.workers.dev \
  -H "Content-Type: application/json" \
  -d '{
    "incidentTitle": "Payment API 503 Spike",
    "logs": "2025-01-15T14:32:11Z ERROR payment-service: connection pool exhausted (max=50)
2025-01-15T14:32:11Z WARN payment-service: retry 1/3 failed for tx_id=abc123
2025-01-15T14:32:15Z ERROR payment-service: upstream db timeout after 30s
2025-01-15T14:32:20Z FATAL payment-service: circuit breaker opened
2025-01-15T14:33:00Z INFO payment-service: circuit breaker half-open, probing
2025-01-15T14:33:05Z INFO payment-service: connection pool restored, circuit closed"
  }'

You’ll get back Markdown like:

## Summary
At 14:32 UTC, the payment service experienced a cascading failure triggered by database connection pool exhaustion. The circuit breaker opened after repeated timeouts, causing a brief outage. Service self-recovered by 14:33 UTC.

## Root Cause
The connection pool (max=50) was fully saturated, likely due to a slow upstream query or connection leak. Retry logic exacerbated the load, and the 30-second database timeout triggered the circuit breaker.

## Timeline (UTC)
- 2025-01-15T14:32:11 — Connection pool exhausted
- 2025-01-15T14:32:15 — Upstream database timeout
- 2025-01-15T14:32:20 — Circuit breaker opened
- 2025-01-15T14:33:00 — Circuit breaker half-open, probing
- 2025-01-15T14:33:05 — Connection pool restored, circuit closed

## Impact
Payment processing was unavailable for approximately 49 seconds. Users would have seen 503 errors during checkout.

## Action Items
- [ ] Increase connection pool size from 50 to 100 as immediate mitigation
- [ ] Add connection pool metrics and alert at 80% utilization
- [ ] Investigate slow query at 14:32 for optimization

That’s a working postmortem draft from six lines of log input. The model correctly identified the cascading failure pattern, extracted all timestamps, and suggested actionable fixes—no hallucination because the temperature is low and the prompt constrains the output.

Sensible Extensions

Once the basic endpoint works, here’s where you can take it:

1. Accept raw text instead of JSON – Many logging tools (like kubectl logs) output plain text. Modify the fetch handler to detect Content-Type: text/plain and treat the entire body as the logs field.

2. Add a Slack slash command – Create a Slack app that POSTs to your Worker. Engineers can paste logs directly into a Slack channel with /postmortem [logs]. This is the same pattern we use in our Discord FAQ bot build—replace Discord with Slack and the vector search with your LLM call.

3. Chunk large log files – Workers AI has a context window limit. For logs exceeding ~4,000 tokens, implement chunking: split logs into overlapping segments, summarize each independently, then feed the summaries into a final synthesis call. This is similar to the chunking strategy used in our flashcard generator guide.

4. Store postmortems in R2 – Cloudflare R2 (free tier: 10GB) can persist generated postmortems. Add a UUID to each response and save to an R2 bucket for later retrieval.

5. Switch to a more capable model – If you need better reasoning for complex incidents, swap @cf/meta/llama-3.1-8b-instruct for @cf/deepseek-ai/deepseek-r1-distill-qwen-32b (also free tier eligible). Expect 2-3x longer inference times but significantly better root cause analysis.

Common Pitfalls and Debugging

“AI binding not found” error – You forgot to add the [ai] section to wrangler.toml. The binding name must match env.AI in your code.

Model returns truncated output – The max_tokens is set to 2048. For very complex incidents, increase this to 4096. The free tier supports up to 4096 output tokens for this model.

Timestamps are hallucinated – This happens when the temperature is too high or the logs don’t contain timestamps. Drop temperature to 0.1 and ensure your logs have ISO 8601 timestamps. If logs genuinely lack timestamps, the prompt instructs the model to say so—but it may still hallucinate if temperature is above 0.5.

Cold starts – First request after deployment can take 5-8 seconds due to Workers AI cold start. Subsequent requests are fast (1-3 seconds). To keep a Worker warm, set up a cron trigger that pings it every 5 minutes (free tier includes cron triggers).

Rate limiting – Free tier caps at 10k AI inferences/day and 100k Worker requests/day. If you exceed this, Cloudflare returns 429 errors. For production on-call use, consider upgrading to the $5/month Workers Paid plan for 1M inferences.

FAQ

Q: Can I use this with real production logs containing sensitive data? A: Cloudflare Workers AI processes data in-memory and does not store prompts or responses. However, you should still sanitize logs before sending them to any third-party LLM. Strip PII, API keys, and customer data. The free tier runs in Cloudflare’s shared infrastructure—treat it as you would any external API.

Q: What if my logs are in JSON format (structured logging)? A: The model handles JSON logs fine—just send the raw JSON string. For better results, you can pre-process JSON logs into a more compact format before sending, but it’s not required. The LLM is surprisingly good at parsing nested JSON log lines.

Q: How does this compare to using ChatGPT or Claude for postmortems? A: This approach is free, runs at the edge (low latency), and requires no API key management. The Llama 3.1 8B model is less capable than GPT-4 for complex reasoning, but for the structured summarization task described here, it performs equivalently. If you’re debugging a truly novel failure mode, a larger model might help—but for 90% of incidents (connection pool exhaustion, OOM kills, timeout cascades), 8B parameters is plenty.

Q: Can I deploy this without using the command line? A: Yes—Cloudflare’s dashboard has a Workers editor where you can paste the code directly. But for iterative development, the CLI with wrangler dev (local development with live reload) is far more productive. This mirrors the debugging workflow we cover in our guide on solving customer issues without environment access.

Q: What’s the maximum log size I can send? A: The Worker body size limit is 100MB, but the practical limit is the LLM context window (~8k tokens for Llama 3.1 8B, roughly 6,000 words). For larger logs, implement the chunking extension described above.

Q: Why not use a streaming response? A: Workers AI supports streaming, but for a postmortem generator, streaming adds complexity without much benefit. The full response is typically under 1,000 words and arrives in 2-3 seconds. Non-streaming keeps the code simpler and the response atomic.

#incident-management#sre#cloudflare-workers#postmortem#summarization

Want to build like a Forward Deployed Engineer?

FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.

Explore the program

More build guides

August 15 · 0d left
Enroll Now