effidevFlutter · Cloudflare edge · Cloud cost optimization

Cloudflare Workers vs. AWS Lambda Cost Comparison: Real Numbers for a Flutter Backend at 1M–100M Requests a Month

Cloudflare Workers vs. AWS Lambda Cost Comparison: Real Numbers for a Flutter Backend at 1M–100M Requests a Month

If you’ve ever searched “Cloudflare Workers vs AWS Lambda cost comparison” while picking a serverless backend for a Flutter app, you’ve probably noticed that most posts end with a hand-wavy verdict like “Workers is cheaper” or “Lambda has a bigger ecosystem.” In reality, the winner flips depending on the nature of the workload and the scale of traffic. This post looks at three use cases that come up constantly in Flutter backends — a push-notification server (calling FCM/APNs), a webhook handler (verifying RevenueCat, Stripe, or App Store Server Notifications), and a general-purpose API server — and works out the actual bill at 1 million, 10 million, and 100 million requests a month, accounting for the billing-model differences between the two platforms and the extra cost of API Gateway.

Key takeaways

  • Cloudflare Workers only bills for CPU time in use; time spent waiting on an external API response never counts against the bill. AWS Lambda bills the entire wall-clock execution time, including wait time, multiplied by the memory allocated to the function.
  • Lambda’s request and compute charges are permanently free up to 1 million requests + 400,000 GB-seconds a month (Always Free). The Workers free plan only covers 100,000 requests a day (roughly 3 million a month) with a 10ms CPU-time cap per invocation — once you cross that CPU limit, you’re forced onto the Paid plan, which starts at $5/month.
  • Exposing Lambda over HTTP by adding API Gateway costs $1.00 per million requests for the HTTP API, or $3.50 per million for the REST API — on top of Lambda’s own charges. For a single endpoint, a Lambda Function URL eliminates this cost entirely. Workers has no equivalent line item at all, since routing is built into the runtime.
  • Under this post’s assumptions (300ms wall-clock per request, 20ms actual CPU time, 256MB memory), Workers becomes cheaper at around 5.5 million requests a month if you’re using API Gateway, or around 11 million requests a month if you’re using a Function URL. Below that, Lambda wins.
  • Conversely, for CPU-bound functions like image resizing, Lambda wins across the entire range at a low memory setting (256MB). That edge narrows, though, for heavier workloads that need 1.1GB–1.4GB of memory or more just to get enough CPU power.

The fundamental billing difference: CPU time vs. total execution time

The first thing to understand when comparing these two platforms is that what counts as “time” for billing purposes is different.

Per Cloudflare’s official pricing docs, Cloudflare Workers only bills for request count and CPU time. While a Worker is waiting on a fetch call to an external API, the V8 isolate isn’t actually executing instructions — it’s idle — so that stretch never shows up on the bill at all. Even if a request chains five external API calls and takes 2 full seconds end to end, if the actual JS execution inside that window (parsing, serialization, branching, etc.) adds up to 20ms, that’s exactly what gets billed: 20ms. The default CPU cap per invocation is 30 seconds, which can be raised to as much as 5 minutes (300,000ms) on the Paid plan, and Cron Triggers and Queue consumers get up to 15 minutes.

AWS Lambda, by contrast, measures the total time from when the handler starts to when it returns a response on a wall-clock basis, and multiplies that by the memory allocated to the function (in GB) to arrive at a GB-second charge. Because the execution environment is considered “running” the entire time it’s waiting on an external API response, that wait time gets billed just the same as compute time. So for the exact same webhook handler, whether the external API responds in 250ms or 800ms translates directly into a difference in GB-second cost for Lambda. Lambda’s execution time is rounded up to the nearest 1ms, function timeouts cap out at 900 seconds (15 minutes), and memory can be set anywhere from 128MB to 10,240MB in 1MB increments. Worth noting: 1,769MB is widely known as the point that corresponds to one full vCPU of compute power, which is why CPU-heavy functions are commonly deployed at or above that threshold.

The practical takeaway from this structural difference is straightforward: the longer a workload spends waiting on external APIs or a database (I/O-bound work), the more it favors Workers; the longer it spends on pure computation (CPU-bound work), the narrower — or even reversed — the gap between the two platforms becomes. The push-notification servers (calling the FCM HTTP v1 API) and webhook handlers (verify a signature, then call an external API) that show up constantly in Flutter backends are textbook I/O-bound workloads.

Same logic, different code skeleton

The code you actually deploy to each platform looks nearly identical. Below is a Worker, written with Hono, that receives a RevenueCat/Stripe-style webhook, verifies its signature, and fires off a push through FCM.

// Cloudflare Worker (Hono)
import { Hono } from "hono";

type Bindings = { WEBHOOK_SECRET: string; FCM_PROJECT_ID: string };
const app = new Hono<{ Bindings: Bindings }>();

app.post("/webhooks/push", async (c) => {
  const rawBody = await c.req.text();
  const signature = c.req.header("X-Signature") ?? "";

  if (!(await verifyHmacSignature(rawBody, signature, c.env.WEBHOOK_SECRET))) {
    return c.text("invalid signature", 401);
  }

  const event = JSON.parse(rawBody);
  const accessToken = await getFcmAccessToken(c.env); // service account JWT exchange

  await fetch(
    `https://fcm.googleapis.com/v1/projects/${c.env.FCM_PROJECT_ID}/messages:send`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ message: buildFcmMessage(event) }),
    },
  );

  return c.json({ ok: true });
});

export default app;

Porting this to Lambda + Function URL only changes the trigger mechanism — the logic stays the same.

// AWS Lambda (Node.js, Function URL trigger)
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";

export const handler = async (
  event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyResultV2> => {
  const rawBody = event.body ?? "";
  const signature = event.headers["x-signature"] ?? "";

  if (!(await verifyHmacSignature(rawBody, signature, process.env.WEBHOOK_SECRET!))) {
    return { statusCode: 401, body: "invalid signature" };
  }

  const payload = JSON.parse(rawBody);
  const accessToken = await getFcmAccessToken();

  await fetch(
    `https://fcm.googleapis.com/v1/projects/${process.env.FCM_PROJECT_ID}/messages:send`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ message: buildFcmMessage(payload) }),
    },
  );

  return { statusCode: 200, body: JSON.stringify({ ok: true }) };
};

At the code level, the developer experience is essentially identical. Everything this post is actually about comes down to one question: who measures the time spent handling a single request, and how, when they write the bill.

Free tier comparison: generous on paper, with a catch on both sides

Item Cloudflare Workers (Free) AWS Lambda (Always Free)
Request limit 100,000/day (~3M/month) 1M/month
Compute limit 10ms CPU time per invocation 400,000 GB-seconds/month
Wait-time billing None (not billable by design) Yes (included in wall-clock time)
Duration Permanently free Permanently free (separate from the 12-month credit for new accounts)
Over the limit Must upgrade to Paid plan ($5/month+) Pay-as-you-go for the overage

Looking at the raw numbers, Lambda’s “1 million requests + 400,000 GB-seconds, permanently free” sounds far more generous, but the real catch on the Workers Free plan isn’t the request count — it’s the 10ms CPU-time cap per invocation. Simple JSON parsing and a response can easily finish inside 10ms, but as soon as you add HMAC signature verification, schema validation with something like Zod, or JWT decoding, real-world code crosses that 10ms line surprisingly easily. Once you go over it, the request itself gets cut off with a CPU-limit error, so it’s fair to say that any API or webhook handler with actual logic in it effectively forces you onto the $5/month Paid plan. Lambda, on the other hand, can genuinely run at $0 if your traffic fits inside the free tier — even with generous memory and time settings.

The API Gateway trap: Lambda routing comes with a hidden bill

A Lambda function on its own doesn’t understand HTTP. To call it over HTTPS from a browser or a Flutter app, you need to put something in front of it — and that choice has a bigger impact on total cost than you’d expect.

Webhook handlers and push-notification servers in a Flutter app are typically single-purpose functions that need only one endpoint (or a small handful). In that case, API Gateway’s routing and usage-management features aren’t needed in the first place, so a Function URL is enough. On the other hand, if you’re building a general-purpose API server that stitches together several Lambda functions under one API, issues API keys to external partners, and manages quotas, you actually need API Gateway.

Cloudflare Workers doesn’t have this distinction at all. Whether you handle multiple routes through the route configuration in wrangler.toml or through a router inside the Worker code (Hono, itty-router, etc.), the per-request charge stays the same.

# wrangler.toml — Workers has no separate charge for routing
name = "push-webhook-worker"
main = "src/index.ts"
compatibility_date = "2025-01-01"

routes = [
  { pattern = "api.example.com/webhooks/*", zone_name = "example.com" },
  { pattern = "api.example.com/push/*", zone_name = "example.com" }
]

Billing simulation by traffic tier: 1M / 10M / 100M requests a month

The numbers below are based on the following assumptions. Actual figures vary by service, so it’s worth plugging your own numbers into the formulas further down and checking for yourself.

Scenario A: I/O-bound (webhook handlers, push-notification servers)

Requests/month Workers (Paid $5+) Lambda compute only Lambda + Function URL Lambda + API Gateway (HTTP API)
1M $5.00 $0.00 $0.00 $1.00
10M $8.40 $7.63 $7.63 $17.63
100M $71.40 $138.13 $138.13 $238.13

Scenario B: CPU-bound (image resizing, signing/encryption, etc.)

Requests/month Workers (Paid $5+) Lambda compute only Lambda + Function URL Lambda + API Gateway (HTTP API)
1M $8.00 $0.00 $0.00 $1.00
10M $40.40 $3.47 $3.47 $13.47
100M $391.40 $96.47 $96.47 $196.47

Put the two tables side by side and something interesting shows up. In Scenario A (I/O-bound), Lambda is dramatically cheaper at low traffic, then Workers overtakes it as traffic scales up. In Scenario B (CPU-bound), Lambda stays cheaper across the entire range at this 256MB assumption. The next section digs into why, and pins down the exact break-even points.

Why the winner shifts depending on the nature of the workload

Here are the actual formulas used for the calculations above, laid bare — plug in your own workload’s numbers and rerun them yourself.

Workers monthly cost (USD) = 5
  + max(0, requests_millions - 10) × 0.30
  + max(0, cpu_ms_per_request × requests_millions - 30) × 0.02

Lambda compute cost (USD) = max(0, requests_millions - 1) × 0.20
  + max(0, total_GB_seconds - 400,000) × 0.0000166667

total_GB_seconds = requests × memory(GB) × avg_wall_clock_seconds

Plugging Scenario A’s numbers into these formulas and solving for where the two curves cross shows that, comparing compute cost alone, Workers starts beating Lambda at around 11 million requests a month. Add API Gateway (HTTP API, $1.00 per million requests) on top, and Lambda’s cost curve gets steeper, pulling the break-even point down to roughly 5.5 million requests a month — nearly half as far. In other words, the single choice of “API Gateway vs. Function URL” affects total cost about as much as the underlying difference between CPU-time billing and wall-clock billing.

The reason Lambda keeps winning in Scenario B (CPU-bound) is simple. At a low memory setting of 256MB (0.25GB), the GB-second rate is tiny (0.2s × 0.25GB × $0.0000166667 ≈ $0.00000083 per request) — cheaper, in fact, than Workers’ per-CPU-ms rate (180ms × $0.02 per million ms ≈ $0.0000036 per request). But this result depends heavily on that 256MB memory assumption. Solving the same formulas in reverse shows that for workloads that need roughly 1.1GB–1.4GB of memory or more (and in practice, image resizing with a library like sharp commonly runs at 512MB–1,769MB to get enough CPU power), this Lambda advantage narrows or disappears, and the balance tips back toward Workers as traffic grows. That’s exactly why you shouldn’t assume “CPU-bound, therefore Lambda, no question” — you need to plug in the actual memory setting you’ll deploy with.

Easy-to-miss extra costs: subrequest limits and data-transfer fees

Push-notification servers often make several outbound calls per request to external APIs like FCM or APNs. Cloudflare doesn’t bill these separately as “subrequests” (only the single inbound request counts toward your request total), but there is a cap on how many subrequests can be in flight at once. Per the official docs, the Free plan allows 50 per request, and the Paid plan defaults to 1,000 per request (as of the February 2026 change, the default was raised — up to 10,000, and configurable up to as much as 10 million). If your architecture fans out to individual requests across hundreds of device tokens, it’s worth checking this limit ahead of time.

Another item that’s easy to overlook in practice is data-transfer (egress) cost. AWS charges $0.09/GB separately for response traffic leaving the region (this is the standard AWS data-transfer fee, and it applies the same way to both API Gateway and Lambda). Cloudflare Workers’ pricing has no equivalent line item for response egress at all. For webhook and push use cases where the response payload is small, this difference is negligible — but for an API server returning large JSON payloads, it adds up as traffic grows.

Conclusion: what should you actually use for a Flutter push-notification server or webhook handler

In the end, there’s no single right answer to “Cloudflare Workers vs AWS Lambda cost comparison.” Traffic scale, the ratio of wait time to CPU time per request, and your choice of routing layer — the only genuinely accurate answer comes from plugging these three variables into the formulas in this post yourself.