Serverless compute: Lambda and function-as-a-service

You write a function. The cloud provider runs it when triggered — by an HTTP request, a scheduled timer, or an event. You pay only for execution time. No servers to manage, no containers to orchestrate. What Lambda is, how it differs from always-on backends, and when to use it.

Beginner

6 min read

The serverless premise

A traditional backend runs on a server (or container) that's always on — consuming compute capacity and costing money even when no requests arrive. Serverless flips this: you write a function (e.g., a Node.js handler), upload it to your cloud provider, and they run it only when triggered. You pay for the CPU-seconds you actually consume, not for idle capacity.

AWS Lambda is the most mature example. You write:

export const handler = async (event, context) => {
  const name = event.queryStringParameters?.name || "World";
  return {
    statusCode: 200,
    body: JSON.stringify({ message: `Hello, ${name}!` }),
  };
};

Upload it; configure a trigger (e.g., an HTTP API, an S3 upload, a scheduled timer); and AWS runs it whenever that trigger fires. The function executes, you're billed for the milliseconds it ran, and that's it. No servers to patch, no containers to manage, no infrastructure overhead.

How Lambda differs from a traditional backend

AspectAlways-on serverLambda
ExecutionContinuously runningOnly when triggered
CostPer hour (or month) of uptimePer millisecond of runtime
ScalingManual (add more servers)Automatic (AWS provisions as needed)
Cold startsNo delay — already runningDelay on first invoke (≈100ms)
Duration limitUnlimited15 minutes max (900 seconds)
State between invokesCan maintain in-memory stateStateless (globals reset)

Lambda is ideal for bursty workloads (processing user uploads, sending emails) and background jobs. It's awkward for long-running processes (a 10-hour data pipeline) or workloads that need persistent in-memory caches.

Triggers and event sources

Lambda functions run when something triggers them. Common triggers:

  • HTTP API (API Gateway) — someone makes a REST request; Lambda handles it and returns a response.
  • Scheduled (EventBridge Scheduler) — a cron-like timer fires every day at 2am; Lambda runs.
  • S3 event — someone uploads a file to an S3 bucket; Lambda is invoked with details about the upload.
  • SQS queue — messages arrive in a queue; Lambda polls and processes them in batches.
  • DynamoDB stream — a database record is created/updated; Lambda reacts.

Each trigger provides an event — data about what happened — passed to the handler function. The handler processes the event and returns a response (if synchronous) or succeeds silently (if asynchronous).

The cold-start problem

When you invoke a Lambda for the first time, or after idle time, AWS must:

  1. Provision a container.
  2. Download and extract your function code.
  3. Start the runtime (Node, Python, etc.).
  4. Run your code.

This takes roughly 100–500ms depending on function size and language — the cold start. Subsequent invocations on the same container reuse it and are much faster.

Cold starts are invisible for user-initiated requests that are okay with a 200–300ms delay. But for something like a WebSocket connection or a financial transaction that expects sub-50ms latency, a cold start is noticeable and bad. Mitigation strategies:

  • Keep code small: fewer dependencies = faster downloads.
  • Use faster languages: Go or Rust cold-start faster than Python.
  • Provisioned concurrency: pay a small ongoing fee to keep N containers warm and ready.
  • CloudFront caching: if possible, cache responses at the edge instead of calling Lambda.

Timeout and resource limits

Lambda functions have hard limits:

  • Max execution time: 15 minutes (900 seconds) — if your function runs longer, Lambda kills it. If you need longer-running jobs, use a different service (ECS, batch jobs, Step Functions).
  • Memory: you choose 128 MB to 10,240 MB; more memory also gives more CPU. Billed by memory × time, so choosing more memory can be cheaper if it lets your function finish faster.
  • Disk space (/tmp)**: 10 GB of temporary storage.
  • Network: no hard limit, but very high egress costs.

The business model: when is Lambda cheap?

Lambda pricing is roughly:

  • $0.20 per 1 million requests (the HTTP calls that trigger your function).
  • $0.0000166667 per GB-second (memory × time your function runs).

If your function uses 512 MB and runs for 100 ms per request, that's 0.05 GB-seconds per request, or roughly $0.0000008 per request in compute costs.

Lambda is cheap when:

  • You have bursty traffic — invoked rarely, scaled elastically.
  • Short execution times — functions that finish in milliseconds or seconds.
  • Predictable load — you can estimate monthly invocations and compute costs.

Lambda is expensive when:

  • Your function runs constantly (then an always-on container or server is cheaper).
  • You need sustained high throughput (millions of invocations per month across a large fleet).
  • Each invocation takes a long time.

Writing Lambda handlers safely

A few patterns that matter:

Keep business logic in libraries, not the handler:

// BAD — handler contains the logic
export const handler = async (event) => {
  const db = await connectDB();
  const user = await db.query("SELECT * FROM users WHERE id=?", [event.userId]);
  // ... 50 more lines of logic
};
 
// GOOD — handler delegates to a library
import { getUserProfile } from "./user-service";
 
export const handler = async (event) => {
  try {
    const profile = await getUserProfile(event.userId);
    return { statusCode: 200, body: JSON.stringify(profile) };
  } catch (error) {
    return { statusCode: 500, body: "Error" };
  }
};

This keeps your code testable — you test getUserProfile with fake databases, and the handler is just a thin orchestration layer.

Reuse database connections across invocations:

let dbPool = null;
 
const getPool = async () => {
  if (!dbPool) {
    dbPool = await createPool({ max: 2 }); // Small pool — this is one Lambda instance
  }
  return dbPool;
};
 
export const handler = async (event) => {
  const pool = await getPool();
  const user = await pool.query(...);
  // Don't close the pool — reuse it on the next invocation
};

The first invocation initializes the pool; later invocations reuse it. This saves connection-setup time and keeps database connection counts manageable.

The invisible tradeoff

Serverless seems "free" until you look at the bill. A Lambda function running constantly (1 million invocations per day × 365 days at 1 second each = 11,574 GB-seconds per month) costs far more than an always-on $5/month server. Measure and benchmark; don't assume serverless is cheaper just because there's no server to manage.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. How is Lambda billing different from an always-on server?

2. What is a cold start, and why does it matter?

3. What is Lambda's maximum execution time, and what should you do if you need longer?

4. Why should Lambda business logic be in libraries rather than directly in the handler?