If you build serverless apps on AWS and your backend happens to be .NET, the way Lambda treats your classes will bite you at some point. Even seasoned engineers trip over it the first time—and then wonder why their state never resets. The fix is boring and reliable once you understand how the two systems interact.
What happened
- Lambda executes functions in three sequential stages: a startup window (up to 10 seconds), a running window, and a cleanup window (up to 2 seconds).
- For .NET handlers, the framework creates one object instance during startup and reuses it for countless calls.
- Initialization logic runs a single time per object, not per trigger.
- Fresh objects appear after any code update and whenever Lambda adds capacity for incoming traffic.
- The safe pattern: make your handler class stateless, reserving the constructor for thread-safe resources meant to persist.
The trap: how .NET mental models break in Lambda
In a traditional .NET application, a class instance typically serves one logical unit of work before being discarded. A web controller arrives, executes, and disappears. Developers习惯性地 carry this expectation into Lambda—without realizing the container model flips the script.
Imagine a handler with a simple numeric tracker, initialized to zero when the object is constructed. Each time the handler fires, the tracker increments. Sequential calls produce results like 1, 2, 3, and so on—because the same object instance handles every request. The initialization never repeats.
What the runtime actually does
Lambda operates in distinct stages. The startup window creates your class, executes your constructor, and configures the environment—capped at 10 seconds. The running window delivers the event to your handler and waits for completion or timeout. The cleanup window (maximum 2 seconds) tears down before the container disappears.
For .NET functions specifically, Lambda builds one object instance during startup and routes all subsequent events to that same instance. Your constructor fires exactly once. It will not fire again unless Lambda tears down the container and provisions a replacement—which happens after deployments or during autoscaling events.
Consider what this means in practice. Fire two requests simultaneously after deploying fresh code, and Lambda spins up two separate instances. Both return their tracker showing 1. But send requests sequentially to the same warm container, and the tracker climbs: 1, 2, 3, incrementally.
When containers get replaced
Three events force Lambda to build new objects:
- Deploying new code** – Any update retires all existing containers.
- Autoscaling under load** – When simultaneous requests exceed available capacity, Lambda provisions additional containers automatically.
- Scheduled teardown** – Lambda recycles warm containers periodically.
You cannot control the timing. Your code must function correctly regardless of whether Lambda gives you a fresh object or a warmed one.
Constructor vs. Handler: what goes where
| Constructor (fires once per container) | Handler (fires once per event) |
|---|---|
| Database connections (DynamoDB, S3) | Per-request data |
| Reusable HTTP clients | Counters and accumulators |
| Configuration caches | Session flags or user context |
| External service stubs | Temporary working variables |
The constructor suits expensive, thread-safe objects you want to reuse across calls. The handler suits everything that belongs to a single request—data that must reset with each trigger.
Who is exposed
Node.js, Python, and Go developers face fewer surprises here. Those environments lack the class-instance model that makes this trap so easy to fall into, though global/module-level state in Node.js can produce analogous bugs.
.NET and Java developers sit squarely in the danger zone. Class-level fields are the culprit. The real risk emerges under production load: a cached user identifier or cart object at class scope can bleed between requests silently. No exceptions fly. The bug just produces wrong answers intermittently—which makes it notoriously hard to track down.
Why it matters
The rule is straightforward: treat your Lambda class like a permanent singleton. Fields declared at class level persist across calls, get shared among concurrent requests on warm containers, and vanish only when Lambda destroys the container.
This forces two hard constraints. Never assume class-level state resets. Treat every value your handler receives as untrusted input, including anything you might have cached. Reserve the constructor for thread-safe, reusable resources only. Counters, flags, and anything request-specific belong inside the handler where they naturally reset.
The payoff is predictability. Stateless handlers survive cold starts gracefully, scale without surprises, and eliminate an entire category of concurrency bugs that only manifest under traffic spikes.
What to watch next
Keep an eye on your CloudWatch metrics for initialization duration spikes followed by stable execution times. That pattern reveals when Lambda provisioned a fresh container. Use it as a trigger to verify your handler still behaves correctly after deployments.
Build stateless, share only thread-safe resources, and the Lambda .NET lifecycle becomes a feature instead of a footgun.
Key takeaways
- Initialization logic fires once per container, not per invocation.
- Lambda reuses objects until it recycles them.
- Per-request data must live inside the handler, never at class scope.
- The constructor belongs to reusable, thread-safe connections.
- Fresh objects arrive on deployments and during autoscaling events.
Frequently asked questions
Does initialization logic run each time my function triggers?
No. It executes exactly once when Lambda first creates the container. The same object handles all subsequent triggers until Lambda either recycles the container or spins up additional ones.
How should I handle database connections that I want to reuse?
Build them in the constructor and ensure they are thread-safe. Lambda reuses the container, so your connection persists across calls—avoiding the cost of rebuilding on every trigger.
My tracker value keeps climbing. Why?
The property lives at class scope while Lambda reuses the same object. Moving that variable inside the handler makes it local to each individual call.
When would Lambda give me a fresh object?
After any deployment, when autoscaling adds capacity for heavy traffic, and when the runtime retires a warm container. Timing is unpredictable, so your code must work correctly either way.
Is this a .NET-specific problem?
The shared-state hazard is most acute in .NET and Java, where class fields persist across calls. Node.js, Python, and Go can stumble with global/module-level variables, but the .NET object model makes this particular trap especially easy to miss.