Warm start

AWS Lambda reuses execution environments (“containers”) between invocations. Everything initialized outside the handler survives to the next call — and Processor initialization (config assembly, possibly a DynamoDB lookup, client construction) is exactly the kind of work you want to pay for once per container, not once per request.

sosw packages this pattern into two small pieces:

from sosw.app import LambdaGlobals, get_lambda_handler


class Processor(SoswProcessor):
    ...


global_vars = LambdaGlobals()
lambda_handler = get_lambda_handler(Processor, global_vars)

What the generated handler does

On every invocation, the lambda_handler returned by sosw.app.get_lambda_handler():

  1. Optionally raises the logging level from the logging_level key of the event.

  2. Resolves the effective test flag from the test key of the event (an explicit flag always wins; otherwise derived from the STAGE environment variable).

  3. Stores the fresh Lambda context in global_vars.lambda_context — the context is unique per invocation, even though the Processor is not.

  4. Constructs the Processor only if global_vars.processor is None — i.e. once per container. Warm invocations reuse the cached instance.

  5. Calls the Processor with the event and captures the result.

  6. Logs get_stats() and calls reset_stats(recursive=True) — exactly once.

  7. Returns the result.

The contract: one Processor per container

LambdaGlobals is a tiny namespace that survives for the lifetime of the Lambda container. It holds two things:

  • global_vars.processor — the cached Processor instance;

  • global_vars.lambda_context — the context of the current invocation (reset by the handler every call).

This gives you a simple set of rules to write correct warm-start code:

  • Per-invocation state belongs in self.result (reset every call), local variables, or the event itself. Never leave request-scoped data in instance attributes: the next invocation of the warm container will see it.

  • Container-lifetime state — clients, caches, compiled regexes, the assembled config — belongs on the Processor instance and is the whole point of the pattern.

  • Counters follow the stats / result contract: self.stats values are folded into total_* accumulators after every invocation.

  • The Lambda context must always be read through global_vars.lambda_context, never cached: each invocation brings a new one (remaining time, request id…).

Warning

LambdaGlobals instances proxy module-level globals of sosw.app: constructing a new LambdaGlobals() resets the shared lambda_context placeholder. Create exactly one instance, at the module level of your app.py, and pass it to get_lambda_handler.

The test flag

Both the Processor constructor and the generated handler resolve the effective test mode with the same rule (sosw.app._derive_test_flag()):

  • an explicitly provided flag always winstest=False passed in a test stage stays False, test=True passed in production stays True;

  • when no flag is provided, it derives from the environment: STAGE set to test or autotest means test mode.

Note

In the 0.7.x line an operator-precedence bug inverted this rule in both directions (an explicit flag was ignored whenever the STAGE said otherwise). This is fixed — see the migration guide.

Cold starts still exist

The first invocation of every container pays the full price: package import plus Processor construction. To keep it low:

  • keep your deployment package small — mount sosw from a shared Lambda Layer (Lambda Layer);

  • skip the external config lookup when you do not need it (disable_ddb_config — see Configuration);

  • initialize only the clients you actually use (init_clients), and use lazy factories like get_ddbc() for the rest.