Node on Lambda

A Lambda handler is an exported async function; Lambda runs it per event and keeps the module alive between invocations.

Syntaxexport const handler = async (event, context) => result;

AWS Lambda runs a function in response to an event, with no server you start or keep alive. You upload code, AWS runs it when something arrives, and you pay for the time it runs.

The handler

The handler is the exported function Lambda calls. It receives event, the input data, and optionally context, information about this invocation. Both module systems work, and these two are the same function:

// ES module, in index.mjs
export const handler = async (event, context) => ({ ok: true });

// CommonJS, in index.cjs
exports.handler = async (event, context) => ({ ok: true });

The function's Handler setting is the file name and the export, joined by a dot: index.handler. Whatever you return is the response. Throwing marks the invocation failed.

Older code used a longhand third argument, (event, context, callback), and called callback(null, result). The nodejs24.x runtime no longer supports it. Write async handlers.

Cold starts

An execution environment is the sandbox Lambda creates to run your code. The first invocation in a new one must download your package and run your module top level before the handler is called. That extra time is a cold start. The environment is then kept for later invocations, so top-level work happens once rather than per event. Create SDK clients and read config there, not inside the handler.

When a function fits

Lambda suits event-driven, bursty work: a queue message, an uploaded file, a scheduled job. A long-running server suits steady traffic, WebSocket connections and a database connection pool that wants to stay open.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Resize an image whenever one is uploaded to an S3 bucket.
  • Run a nightly report without paying for a container that idles all day.
  • Handle a webhook from a payment provider that arrives a few times a day.
  • Drain an SQS queue with concurrency that scales itself up and back to zero.

More examples

Setup outside the handler

Moving the client out of the handler is the cheapest Lambda improvement there is: a new client per invocation re-resolves credentials and opens fresh connections every time. Throwing on missing config during init fails the invocation with a clear error instead of producing a confusing one later. Keep per-user data out of module scope, since the next event may belong to someone else.

Example · javascript
// index.mjs
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

// Module top level: runs once per execution environment, during init.
// The client, its credential cache and its sockets survive between events.
const s3 = new S3Client({});
const BUCKET = process.env.RECEIPT_BUCKET;
if (!BUCKET) throw new Error("RECEIPT_BUCKET is not set"); // fail at init

// ES modules allow top-level await, so slow setup finishes during init
// rather than stealing time from the first request.
// const config = await loadConfig();

export const handler = async (event) => {
  await s3.send(new PutObjectCommand({
    Bucket: BUCKET,
    Key: `receipts/${event.orderId}.txt`,
    Body: `order ${event.orderId}`,
  }));
  return "ok";
};

Separating the handler from the logic

A thin handler keeps your real logic in functions you can test with node --test and no fake event objects. The context check uses getRemainingTimeInMillis, which returns the milliseconds left before Lambda stops the invocation: refusing to start work you cannot finish is better than being cut off halfway through a write.

Example · javascript
// quote.mjs  — plain function, no Lambda types, easy to unit test
export function quote({ plan, seats }, prices) {
  if (!(plan in prices)) throw new Error(`unknown plan: ${plan}`);
  return { amount: prices[plan] * seats };
}

// index.mjs  — the handler only translates event to arguments and back
import { quote } from "./quote.mjs";
const PRICES = { pro: 4900, team: 9900 };

export const handler = async (event, context) => {
  // Budget check: stop before Lambda kills the invocation mid-write.
  if (context.getRemainingTimeInMillis() < 1000) {
    throw new Error("not enough time left");
  }
  return quote(event, PRICES);
};

// quote.test.mjs — runs with node --test, no AWS involved
// import { quote } from "./quote.mjs";

What the context object carries

Putting awsRequestId in every log line is what lets you pull one invocation out of a log group later, which is the Lambda equivalent of a request ID in a server. console.log writes to stdout and Lambda forwards it to CloudWatch Logs, so JSON on one line is searchable while a multi-line object is not.

Example · javascript
export const handler = async (event, context) => {
  // Properties that are useful in logs and in support requests.
  console.log(JSON.stringify({
    requestId: context.awsRequestId,       // unique per invocation
    functionName: context.functionName,
    functionVersion: context.functionVersion,
    memoryLimitInMB: context.memoryLimitInMB,
    logGroupName: context.logGroupName,
    logStreamName: context.logStreamName,
    msLeft: context.getRemainingTimeInMillis(),
  }));

  return context.logStreamName;
};

Discussion

  • Be the first to comment on this lesson.