Serverless and edge functions

Packaging and triggers, cold starts, stateless design, edge runtime limits, background work, and the pricing shapes that surprise people.

How a function runs

// a handler: one invocation, one response, no shared state to rely on
export async function handler(event, context) {
  const body = JSON.parse(event.body ?? "{}");

  if (!body.email) {
    return { statusCode: 400, headers: { "content-type": "application/json" },
             body: JSON.stringify({ error: "email is required" }) };
  }

  await db.query("insert into subscribers (email) values ($1) on conflict do nothing", [body.email]);

  // anything slow belongs on a queue, not in the response path
  await queue.send({ type: "welcome-email", email: body.email });

  return { statusCode: 202, body: "" };
}
  • Execution environments are reused but not guaranteed. Module-level state survives sometimes, which makes a caching bug appear intermittent.
  • Every invocation needs a fresh database connection unless you use a pooler. Exhausting the database connection limit is the most common serverless outage.
  • The response is the end of the work. Anything that must survive goes to a queue first.
  • Timeouts are hard limits. Check the platform value before designing a request that does a slow third-party call.
Platform limitTypical valueDesign consequence
Execution timeout10-60 s, sometimes less at the edgeLong jobs must be queued
Memory128 MB to several GBImage and PDF work needs the larger tiers
Payload size6 MB for a synchronous requestUploads must go directly to object storage
ConcurrencyAccount-wide, sometimes scaled by memoryOne runaway function blocks everything
Ephemeral disk/tmp, small and per-instanceNo caching large files between calls
Edge runtimeNo native modules, limited Node APIsNot every library runs at the edge

Cold starts and edge runtimes

  1. Measure the cold start for your actual bundle. It grows with dependency count, not with your own code size.
  2. Keep the handler small: import the one client you need rather than a whole SDK, and initialise expensive clients lazily but outside the handler.
  3. Use provisioned concurrency only where a latency budget demands it. It removes the cold start and reintroduces a fixed monthly cost.
  4. Edge runtimes are smaller and start faster but run a restricted JavaScript environment. Check that your dependencies and your crypto code work there before committing.
  5. Warm-up pings do not help reliably. Platforms retire idle instances whenever they choose.
// initialise once per instance, outside the handler
import { Pool } from "pg";

let pool;
function getPool() {
  pool ??= new Pool({
    connectionString: process.env.DATABASE_URL,
    max: 1,                          // one connection per instance
    idleTimeoutMillis: 10000
  });
  return pool;
}

export async function handler() {
  const { rows } = await getPool().query("select count(*) from orders");
  return { statusCode: 200, body: JSON.stringify(rows[0]) };
}

A pool per instance with max: 1, plus an external connection pooler, is the pattern that survives a traffic spike. Without the pooler, a hundred concurrent invocations open a hundred connections and the database refuses them.

Costs and background work

Billing dimensionWhy it surprisesControl
InvocationsA polling pattern multiplies callsPush, or widen the poll interval
Compute timeBilled on wall time including waitingDo not sit in a sleep; queue instead
EgressEvery byte leaving is chargedCompress, cache, and keep data in region
Log volumeVerbose logging is billedLog levels per environment, short retention
Provisioned concurrencyA fixed hourly chargeUse only for latency-critical paths
NAT gatewayCharged per hour plus per GBAvoid unnecessary outbound calls
Queued workA backlog re-tried on failureDead-letter queues and idempotency
Making background work safe

  queue           one message per unit of work
  idempotency key so a retry does not double-charge a customer
  dead-letter     after N attempts, park the message and alert
  visibility      long enough that a slow job is not delivered twice
  schedule        from the platform, not from a self-scheduling function

Every serverless retry is a duplicate unless the handler is idempotent.
⚠️
Scheduled functions and event triggers retry on failure. Never write a handler that appends a row, sends an email or charges a card without an idempotency key - the second attempt looks exactly like the first, and the customer sees the duplicate.

FAQ

Is serverless cheaper?
For spiky or low-volume traffic, usually yes. For steady high-volume traffic, a container is often cheaper because you are not paying a per-request premium. Model both at your actual request volume.
Can I use serverless for a whole application?
Yes, if the request path is short and stateless. The moment you need long-lived connections, large in-memory caches or a persistent process, containers fit better.

Hosting models compared: VPS, PaaS, containers and serverless Monitoring, uptime and log management

Last refreshed 2026-09-18.