AWS Lambda Execution Environments and Cold Starts
AWS Lambda runs each function invocation inside an execution environment: an isolated runtime containing your code, its language runtime, configuration, and temporary filesystem. Lambda may keep that environment after an invocation and send a later invocation to it, but reuse is an optimization, not a guarantee.
This model explains three common observations: a function is sometimes slower on its first call, initialization code appears to run only occasionally, and simultaneous requests can each have their own copy of in-memory state.
The problem Lambda solves
Without reuse, every request would need to create a process, start the language runtime, load dependencies, and run setup code before it could execute your handler. That would make short functions unnecessarily slow and would repeat expensive work on every request.
Lambda separates initialization from invocation. It performs setup when it creates an environment, runs your handler for a request, and can then retain the environment for another request. This reduces latency and avoids repeating setup such as loading a large library or establishing a database connection.
The trade-off is that you do not control the lifetime of an environment. Your function must work correctly whether an environment is reused or discarded immediately.
What happens when an environment is created
For a new environment, Lambda broadly performs these steps:
- It provisions an isolated environment with the configured memory, runtime, environment variables, and other settings.
- It starts the selected language runtime and loads your function code.
- It runs initialization code outside the handler, such as Python imports and module-level statements.
- It starts any configured Lambda extensions.
- It calls the handler with the event and context for the invocation.
The time spent creating the environment and running this setup is commonly called cold-start latency. The handler itself also takes time, so the total duration reported for the first invocation includes both initialization and handler execution. In logs, you may see a REPORT line containing an Init Duration; that is the part attributable to initialization.
For example:
import json
# Runs when this execution environment is initialized.
configuration = load_configuration()
client = make_service_client(configuration)
def handler(event, context):
result = client.process(event["value"])
return {
"statusCode": 200,
"body": json.dumps(result),
}
Here, load_configuration(), make_service_client(), and the import statements run once for a particular environment. The handler function runs for each invocation sent to that environment.
Moving expensive, safe-to-reuse setup outside the handler can reduce the work per invocation. It also increases cold-start time, however, so initialization should not become an indiscriminate place to put every operation.
Warm reuse and persistent state
After a handler returns, Lambda may leave the environment available. A later invocation can then use the same runtime process. This is a warm invocation: it skips the environment's initialization phase and starts at the handler.
Several kinds of state can survive between invocations in that environment:
- Module-level and global variables, including in-memory caches and initialized clients.
- Objects created during initialization or an earlier handler call.
- Files in
/tmp, Lambda's temporary writable filesystem for the environment. - Open network connections, if the runtime, client library, and remote service allow them to remain usable.
This state belongs only to that one environment. It is not a shared cache for the function as a whole, and it is not shared with other environments.
A reused environment can be useful for caching configuration, reusing SDK clients, or avoiding repeated downloads. But correctness must not depend on it. Lambda can remove an idle environment, replace it after an error or maintenance event, or create a new one at any time. Treat memory and /tmp as disposable caches, not durable storage.
The same caution applies to connections. A connection that survived from a previous invocation may have been closed by a database, load balancer, or network device. Code should be prepared to reconnect or retry when a reused connection is no longer valid.
Cold starts and concurrency
A cold start occurs when Lambda must create a new execution environment before it can run an invocation. It commonly happens for a newly deployed function, after an environment has been retired, or when traffic grows beyond the environments currently available.
A single warm environment does not process an unlimited stream of simultaneous requests. When multiple invocations arrive at once, Lambda creates additional environments as needed. Each environment initializes independently and receives one invocation at a time from the function's normal request path.
For example, if five requests arrive concurrently and no suitable environments exist, Lambda can create five environments. Each one runs initialization, has its own globals and /tmp, and handles one request. A later request may go to any available warm environment rather than the one that handled a related request.
This is why an in-memory counter is not a reliable application-wide counter: each environment has a separate copy, and new environments begin with fresh state. Durable coordination belongs in a service designed for it, such as a database or a dedicated cache.
Reducing unpredictable initialization latency
Lambda offers features that change when initialization happens, but not the basic isolation model. Provisioned concurrency keeps a configured number of environments initialized before requests arrive, reducing cold starts for those environments. It does not make all invocations share memory.
Some runtimes can also use snapshot-based startup features, such as Lambda SnapStart, which restore a previously initialized runtime rather than performing every initialization step from scratch. Code using such a feature must still treat restored state carefully: cached data can be stale, and connections may need to be re-established.
The practical rule is simple: initialize reusable resources outside the handler, keep handlers correct with either fresh or reused state, and store anything that must survive environment replacement outside Lambda's execution environment.