In a FaaS handler like AWS Lambda's `handler(event, context)`, why is it considered a best practice to initialize things like a database connection or an AWS SDK client outside the handler function rather than inside it, and what state actually persists between invocations on a warm environment?
answer
- module/global scope code runs once per environment
- handler body runs every invocation
- warm state = opportunistic, not guaranteed
- concurrent invocations get separate environments
- never store per-request data in global scope
basics
~20 sCode written outside the handler function runs once per environment and gets reused across many warm calls, while code inside the handler runs every single time. Putting slow setup (like opening a connection) outside means later calls skip that work and run faster.
solid answer
~50 sA FaaS handler has a fixed signature - typically `(event, context)` per language - where the platform calls your handler function once per invocation, but only re-runs the module/file-level code (imports, global variable assignment, client construction) the first time an environment is created, i.e., on a cold start. Anything declared outside the handler - a database connection pool, an SDK client, a config value read once - stays alive in that environment's memory and is reused on every subsequent warm invocation, avoiding the cost of re-establishing it each call. This also means global/module-scope variables, in-memory caches, and anything written to local disk (like AWS Lambda's `/tmp`) can persist across invocations on the same warm environment, but you can never rely on this because the platform can freeze, reuse, or destroy the environment at any time without notice, and concurrent invocations may or may not share the same environment depending on the provider's concurrency model.
go deeper
Should know the basic rule of thumb: put expensive setup code outside the handler function so it doesn't rerun on every call. Doesn't need to explain the freeze/thaw mechanics.
Should explain why (init code runs once per environment vs. handler runs every invocation) and name what kinds of state persist (global variables, /tmp, open connections), plus know that this reuse isn't guaranteed.
Should identify the concrete failure modes - stale connections/credentials, cross-invocation state leakage across warm environments - and describe how to design handlers defensively (health-checking reused connections, keeping request-scoped state local).
Should reason about this at a systems level: how environment reuse interacts with connection-pool sizing against a downstream database under concurrency, security implications of shared warm state in multi-tenant functions, and when to introduce an external cache/connection broker instead of relying on in-process reuse.
## The handler and the init phase Every FaaS platform exposes a handler with a fixed, narrow signature: - AWS Lambda's Node.js runtime calls `exports.handler = async (event, context) => {...}` - Python calls `def handler(event, context):` - and equivalents exist for Google Cloud Functions and Azure Functions The platform's job is to route an incoming event - an HTTP request, a queue message, a storage trigger - into that function and return its result. Critically, the platform only executes the code inside the handler body on every invocation; any code that lives outside the handler, at module or file scope, runs **exactly once per execution environment** - specifically, during the init phase of a cold start, before the first handler invocation in that environment ever happens. ## Why 'initialize outside the handler' is a best practice This distinction is the whole reason 'initialize outside the handler' is a best practice. If you construct a database connection pool, an AWS SDK client (like a DynamoDB or S3 client), or load a config file **inside** the handler, that setup work repeats on every single invocation - even warm ones - burning time and money and, worse, potentially exhausting a downstream resource like a database's max-connections limit if many concurrent invocations each open their own fresh connection. If you instead declare that client as a **module-level variable**, it is created once when the environment cold-starts, and every subsequent warm invocation in that same environment reuses the already-open connection or already-constructed client object, skipping that setup cost entirely. This is often the single biggest lever for reducing both latency and cost in a FaaS function that talks to external systems. ## What actually persists on a warm environment The underlying mechanism is that the execution environment - the container or microVM the platform spun up - is not torn down and recreated between invocations as long as it stays 'warm.' Its process memory persists from one invocation to the next on that same environment, including: - global variables - in-memory caches (e.g., a hand-rolled cache of feature flags) - open network sockets - anything written to a scratch directory (AWS Lambda exposes up to 512MB-10GB of ephemeral storage at `/tmp`; Cloud Functions and Azure Functions have similar scratch space) This lets you build simple in-memory caches or reuse expensive-to-construct objects (like a compiled regex, a loaded ML model, or a parsed schema) without any external cache service. ## Why you cannot rely on it The trade-off and the reason this requires care rather than blind reliance is that this persisted state is entirely **opportunistic and platform-controlled**, not something your code can depend on. The platform can freeze an environment mid-execution (pausing all threads and timers between invocations - a subtlety that has bitten teams who spawned background async work expecting it to keep running), reclaim an idle environment at any time after some idle window, or - especially under a burst of concurrent traffic - spin up many parallel environments, each with its own independent copy of that 'persisted' state, meaning a global counter or in-memory cache is not shared across concurrent invocations the way it would be across threads in a single long-running server process. You must always write your handler as if it could be a completely fresh, empty environment on every call, treating any warm-state reuse purely as a performance optimization, never as required correctness. ## Two failure modes in production Two concrete failure modes show up in production. - **First, credential or connection staleness**: a database connection or an auth token constructed once at cold start can go stale (the DB restarts, a token expires) while the environment stays warm for hours, and code that assumes the module-scope client is always healthy will throw sporadic errors until the environment eventually recycles - the fix is to add reconnect/refresh logic rather than assuming a single construction is good forever. - **Second, cross-invocation data leakage or contamination**: writing per-request mutable state into a module-level variable (instead of a local variable inside the handler) can leak data from one user's request into another's response if that same warm environment happens to serve a different user's invocation next - a subtle but serious bug, and in multi-tenant systems a potential security issue. ## A concrete real-world pattern A concrete real-world pattern: a Lambda function backing a REST API constructs its DynamoDB client and reads a small config object from Parameter Store once at module scope, then, inside the handler, only performs the per-request logic - reading the event body, calling `dynamoClient.get(...)`, building the response. On a burst of 200 concurrent requests, AWS spins up (up to) 200 separate environments, each doing its own one-time client construction and Parameter Store read, but subsequent traffic reuses those same 200 warm environments and skips that cost - which is exactly why cold-start-heavy bursts followed by sustained traffic show a latency curve that starts high and then drops and stays low.
- What's a concrete bug that can happen if you store per-request mutable state in a module-level (global) variable instead of a local variable inside the handler?Because a warm environment can serve multiple sequential invocations, data written to a global variable during one request can still be present when the next invocation runs on that same environment, potentially leaking one user's data into another user's response. This is especially dangerous in multi-tenant systems and is a real security/correctness bug, not just a style issue.
- Why can't you rely on a background thread or timer you start inside a handler to keep running after you return a response?Once the handler returns, most FaaS platforms freeze the execution environment's process - including any pending timers or unawaited async work - until the next invocation arrives, so that background work may simply never resume, or resume unexpectedly interleaved with the next request. If you need guaranteed background work, you generally need to either await it before returning or push it to a separate mechanism (a queue, a different compute service) designed for it.
Like a barista who sets up the espresso machine and grinds a batch of beans once at the start of their shift (init code), then just pulls a fresh shot per customer (handler body) without re-setting-up the machine every time - but if the shift ends and a new barista comes in (a new environment), that setup has to happen again from scratch.
saying these in an interview costs you the question
- Puts SDK client construction inside the handler and doesn't see why that's wasteful
- Assumes warm-state reuse is guaranteed and writes code that breaks on a cold start
- Stores per-request data in a global/module variable
- Thinks each invocation always gets its own brand-new container
- Unaware that concurrent invocations may run in separate, non-shared environments