Skip to the solution
HardSystem Design9 min read

What changes for a Node.js app running on AWS Lambda vs a long-running server?

774 views

The problem

Question presented to candidate: "Your Express app runs great on a long-lived server. You are asked to port its logic to a single AWS Lambda function. What actually has to change, beyond swapping app.listen() for a handler export?"

What a strong answer should cover:

  • The fundamental shift: a long-running server has one process serving many requests over its entire lifetime. Lambda instead runs your code inside an execution environment that AWS may reuse across several invocations (a "warm" invocation) or may need to create fresh (a "cold start") β€” and your code has no control over which one happens for a given request.
  • πŸ“Œ Execution context reuse: code written outside the handler function β€” imports, a database client, a computed constant β€” runs once per execution environment, not once per invocation. This is the single most important practical consequence: a DB connection pool, an HTTP keep-alive agent, or an expensive computed value should be initialized at module scope specifically so warm invocations can reuse it, instead of re-paying that cost on every request.
  • Cold starts are a real, measurable cost specific to Lambda that a long-running server simply does not have: the first invocation on a fresh execution environment pays for loading the runtime, your code, and any module-scope initialization, before your handler logic even starts.
  • A long-running server can hold in-memory state across requests indefinitely (an in-memory cache, a queue) with a guarantee that it persists as long as the process is up. Lambda offers no such guarantee β€” an execution environment can be reused for a while, then discarded, with no notification to your code; anything relying on in-memory state surviving between requests needs an external store instead (a database, Redis, S3).
  • Lambda gives each execution environment a fixed amount of /tmp ephemeral disk (512 MB by default, configurable up to 10,240 MB / 10 GB as of this writing) that similarly may or may not persist between invocations depending on reuse β€” treat anything written there as disposable, not durable.
  • The handler execution model itself changes control flow: a Lambda handler is invoked once per event and expected to return (or resolve/call a callback) to signal completion β€” there is no long-lived event loop sitatting idle between requests the way app.listen() provides; any work intentionally left running after the handler returns (an unawaited promise, a timer) is not guaranteed to execute, since the environment can be frozen the instant the handler signals completion.

Clarifying questions expected:

  • "Does anything in the current implementation rely on in-memory state surviving between requests?" β€” that specific assumption breaks under Lambda's execution model.
  • "Is this a latency-sensitive, user-facing path where cold starts would be user-visible?" β€” decides how much cold-start mitigation (provisioned concurrency, smaller bundles) is worth the added complexity.

Code / implementation expected: Optional, but demonstrating the module-scope-reuse mechanism itself β€” the actual reason connection pooling works on Lambda β€” with real, observable process behavior is the most convincing thing to show.

  • serverless
  • aws-lambda
  • cold-start
  • architecture

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 8 sections

Read the code

JavaScriptExecution-context reuse, demonstrated directly: same process (warm analog) vs fresh process per call (cold analog)
// lambda-handler.mjs
let initCount = 0;
initCount++; // runs once per PROCESS β€” the module-scope "connection setup"
console.log(`[module scope] init ran β€” initCount is now ${initCount} (pid ${process.pid})`);

export function handler(invocationId) {
  return { invocationId, initCount, pid: process.pid };
}

// --- warm-start analog: import once, call 3 times in the SAME process ---
// import { handler } from "./lambda-handler.mjs";
// handler("req-1"); handler("req-2"); handler("req-3");
//
// [module scope] init ran β€” initCount is now 1 (pid 19408)
// { invocationId: 'req-1', initCount: 1, pid: 19408 }
// { invocationId: 'req-2', initCount: 1, pid: 19408 }
// { invocationId: 'req-3', initCount: 1, pid: 19408 }   <- same pid, init ran ONCE

// --- cold-start analog: a fresh "node -e" process per call ---
// $ node -e "import('./lambda-handler.mjs').then(m => console.log(m.handler('req-A')))"
// $ node -e "import('./lambda-handler.mjs').then(m => console.log(m.handler('req-B')))"
//
// [module scope] init ran β€” initCount is now 1 (pid 35252)
// { invocationId: 'req-A', initCount: 1, pid: 35252 }
// [module scope] init ran β€” initCount is now 1 (pid 14624)   <- DIFFERENT pid, init ran AGAIN
// { invocationId: 'req-B', initCount: 1, pid: 14624 }

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 130 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

How do you achieve concurrency in Node.js since it's single-threaded?

Continue