Skip to content

Bug: Lambda layer ships a partial AWS SDK that shadows the runtime SDK, breaking DynamoDB DocumentClient #5511

Description

@svozza

Expected Behavior

The Lambda layer should not change which AWS SDK packages a customer's function resolves. Either the layer exposes a coherent, complete SDK group, or its bundled SDK is not importable from customer code at all.

In particular, a function using @aws-sdk/lib-dynamodb's DynamoDBDocumentClient should get a matching @aws-sdk/client-dynamodb and @aws-sdk/lib-dynamodb pair.

Current Behavior

The layer installs its dependencies into a flat /opt/nodejs/node_modules. It includes @aws-sdk/client-dynamodb (as a transitive dependency) but not @aws-sdk/lib-dynamodb.

Because Lambda places /opt/nodejs/node_modules before /var/runtime/node_modules on NODE_PATH, a function using the DocumentClient resolves:

  • @aws-sdk/client-dynamodb from the layer
  • @aws-sdk/lib-dynamodb from the runtime

When those two land on opposite sides of the AWS SDK for JavaScript 3.928.0 boundary — which moved serializerMiddleware registration from per-command to per-client — the mismatch fails at runtime:

serializerMiddleware is not found when adding DocumentMarshall middleware before serializerMiddleware

This affected the v1 layer (AWSLambdaPowertoolsTypeScript:27), which shipped @aws-sdk/client-dynamodb@3.454.0.

The current v2 layer has the same structure and is currently benign only because of version polarity: AWSLambdaPowertoolsTypeScriptV2:49 ships client-dynamodb@3.1084.0, which is newer than the runtime's lib-dynamodb@3.1049.0. The split already exists in production. If the runtime SDK rolls forward past the layer's pinned version, the polarity flips and v2 breaks the same way v1 did.

Note the customer never asked for the layer's SDK — they may not import Powertools' DynamoDB features at all. Simply attaching the layer changes their SDK resolution.

Code snippet

// A function with the Powertools layer attached, using the DynamoDB DocumentClient.
// No Powertools import is required to hit this — the layer alone changes resolution.
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
  DynamoDBDocumentClient,
  PutCommand,
} from '@aws-sdk/lib-dynamodb';

// client-dynamodb resolves from /opt/nodejs/node_modules  (the layer)
// lib-dynamodb   resolves from /var/runtime/node_modules   (the runtime)
const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export const handler = async () => {
  // Throws when the two copies straddle the SDK 3.928.0 boundary:
  //   "serializerMiddleware is not found when adding DocumentMarshall
  //    middleware before serializerMiddleware"
  await doc.send(
    new PutCommand({
      TableName: process.env.TABLE_NAME,
      Item: { id: 'example', nested: { a: [1, 'two', true] } },
    })
  );
};

Steps to Reproduce

  1. Create a Node.js Lambda function that does not bundle @aws-sdk/client-dynamodb or @aws-sdk/lib-dynamodb, so both come from outside /var/task.
  2. Attach a Powertools layer whose bundled @aws-sdk/client-dynamodb is older than 3.928.0 (e.g. v1 layer AWSLambdaPowertoolsTypeScript:27, which ships 3.454.0).
  3. Use DynamoDBDocumentClient.from(new DynamoDBClient({})) and send any command.
  4. Observe the serializerMiddleware is not found error.

To confirm the resolution split on any layer version:

const { createRequire } = require('node:module');
const r = createRequire('/var/task/index.js');
console.log('client:', r.resolve('@aws-sdk/client-dynamodb'));
console.log('lib:   ', r.resolve('@aws-sdk/lib-dynamodb'));

Possible Solution

Do not ship any @aws-sdk/* client in the layer, and rely on the AWS SDK provided by the Lambda Node.js runtime.

The runtime already provides a complete, self-consistent SDK v3 — all 547 @aws-sdk packages, including lib-dynamodb — so both halves of any client/lib pair come from one matching set.

Of the two options suggested (expose a complete DynamoDB group, or make the bundle non-importable), removing it is preferable: shipping more artifacts adds further mismatch surface, whereas shipping none removes the layer as a source of version splits entirely.

Verified on nodejs22.x and nodejs24.x, CJS and ESM: all utilities (Logger, Metrics, Tracer including captureAWSv3Client, Parameters, Idempotency, Parser, Batch) work with no @aws-sdk/* in the layer. Note that @smithy/* and zod are not provided by the runtime and must remain in the layer — @smithy/service-error-classification is a runtime dependency of aws-xray-sdk-core.

Side effect: the layer shrinks from 24 MB to 16 MB unpacked (4.3 MB to 2.9 MB zipped).

A/B verification

Two layers, identical except for the presence of the bundled SDK — both contain the same 10 Powertools packages, one additionally contains @aws-sdk/client-dynamodb@3.454.0 with no @aws-sdk/lib-dynamodb. The test function is a plain DocumentClient round-trip that does not use Powertools at all; it only has the layer attached. Runtime nodejs22.x.

Layer with SDK Layer without SDK
@aws-sdk/client-dynamodb resolves from /opt/nodejs @3.454.0 /var/runtime @3.1049.0
@aws-sdk/lib-dynamodb resolves from /var/runtime @3.1049.0 /var/runtime @3.1049.0
DocumentClient round-trip FAILS PASSES
Powertools still loads

The only variable is whether the layer ships an AWS SDK client. Removing it eliminates the failure, and Powertools keeps working in both arms.

Documentation also needs updating

docs/getting-started/lambda-layers.md currently tells customers three times (lines 157, 226, 255 — CDK, SAM and Serverless Framework examples) to exclude @aws-sdk/* from their bundle:

make sure to exclude @aws-lambda-powertools/* and @aws-sdk/* from being bundled since the packages are already present the layer

This guidance is a contributing factor: excluding @aws-sdk/* means the customer does not ship their own SDK, so resolution falls through to the layer's partial copy. Bundling your own SDK into /var/task is the one configuration that is safe on every layer version.

After this change the reasoning also becomes incorrect, since @aws-sdk/* will no longer be present in the layer. All three snippets and the surrounding wording need revisiting.

docs/getting-started/installation.md needs no change — it correctly lists the SDK packages as customer-installed peer dependencies.

Powertools for AWS Lambda (TypeScript) version

latest

AWS Lambda function runtime

22.x

Packaging format used

Lambda Layers

Execution logs

ERROR	Invoke Error 	{
    "errorType": "Error",
    "errorMessage": "serializerMiddleware is not found when adding DocumentMarshall middleware before serializerMiddleware",
    "stack": [
        "Error: serializerMiddleware is not found when adding DocumentMarshall middleware before serializerMiddleware",
        "    at ...",
    ]
}

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingconfirmedThe scope is clear, ready for implementation

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions