Skip to content

Lambda mode shares one CLS context and facade segment across concurrent invocations (Lambda Managed Instances) #760

Description

@dreamorosi

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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions