Summary
In Lambda mode, the SDK creates a single process-wide CLS context at initialization and never creates another (lib/env/aws_lambda.js):
var namespace = contextUtils.getNamespace();
namespace.enter(namespace.createContext());
contextUtils.setSegment(facadeSegment());
With Lambda Managed Instances (perExecutionEnvironmentMaxConcurrency > 1), multiple invocations are multiplexed into the same execution environment concurrently. Two pieces of state are then shared by all in-flight invocations:
-
The active-segment slot. setSegment() writes into the single shared context, so any code that manages subsegments via getSegment()/setSegment() (rather than captureAsyncFunc) resolves another invocation's subsegment once invocations interleave. Annotations, metadata, and instrumented-call subsegments attach to the wrong invocation - potentially another tenant's trace.
-
The facade segment itself. There is one facade per process, and resolveLambdaTraceData() mutates its trace_id/id/notTraced in place whenever getSegment() observes a new trace header. Under interleaving, subsegment flush() reads this.segment.trace_id at emission time, so even captureAsyncFunc users can emit subsegments onto whichever trace the facade was last resolved to.
The runtime already provides the primitive needed to fix this: globalThis.awslambda.InvokeStore, an AsyncLocalStorage that the runtime scopes around each invocation - and which resolveLambdaTraceData() already consults for the trace id.
Proposal
In Lambda mode, when awslambda.InvokeStore is available:
- scope the current-segment storage per invocation (e.g. key the segment slot by
InvokeStore identity instead of the single CLS slot), and
- maintain a per-invocation facade (or at minimum snapshot the resolved trace data onto subsegments at creation time rather than reading the shared facade at flush time),
falling back to today's single-context behavior on runtimes without InvokeStore. This would make both captureAsyncFunc and getSegment()/setSegment()-based instrumentation safe under LMI concurrency without API changes.
Reproduction
Deploy any function using getSegment()/addNewSubsegment()/setSegment() per invocation on a capacity provider with perExecutionEnvironmentMaxConcurrency > 1, add a distinctive annotation per invocation, and force multiplexing: annotations land on other invocations' subsegments, and subsegment documents are emitted with the wrong trace_id.
Downstream report with full trace evidence: aws-powertools/powertools-lambda-typescript#5434.
Summary
In Lambda mode, the SDK creates a single process-wide CLS context at initialization and never creates another (
lib/env/aws_lambda.js):With Lambda Managed Instances (
perExecutionEnvironmentMaxConcurrency> 1), multiple invocations are multiplexed into the same execution environment concurrently. Two pieces of state are then shared by all in-flight invocations:The active-segment slot.
setSegment()writes into the single shared context, so any code that manages subsegments viagetSegment()/setSegment()(rather thancaptureAsyncFunc) resolves another invocation's subsegment once invocations interleave. Annotations, metadata, and instrumented-call subsegments attach to the wrong invocation - potentially another tenant's trace.The facade segment itself. There is one facade per process, and
resolveLambdaTraceData()mutates itstrace_id/id/notTracedin place whenevergetSegment()observes a new trace header. Under interleaving, subsegmentflush()readsthis.segment.trace_idat emission time, so evencaptureAsyncFuncusers can emit subsegments onto whichever trace the facade was last resolved to.The runtime already provides the primitive needed to fix this:
globalThis.awslambda.InvokeStore, anAsyncLocalStoragethat the runtime scopes around each invocation - and whichresolveLambdaTraceData()already consults for the trace id.Proposal
In Lambda mode, when
awslambda.InvokeStoreis available:InvokeStoreidentity instead of the single CLS slot), andfalling back to today's single-context behavior on runtimes without
InvokeStore. This would make bothcaptureAsyncFuncandgetSegment()/setSegment()-based instrumentation safe under LMI concurrency without API changes.Reproduction
Deploy any function using
getSegment()/addNewSubsegment()/setSegment()per invocation on a capacity provider withperExecutionEnvironmentMaxConcurrency> 1, add a distinctive annotation per invocation, and force multiplexing: annotations land on other invocations' subsegments, and subsegment documents are emitted with the wrongtrace_id.Downstream report with full trace evidence: aws-powertools/powertools-lambda-typescript#5434.