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
- 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.
- 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).
- Use
DynamoDBDocumentClient.from(new DynamoDBClient({})) and send any command.
- 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 ...",
]
}
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'sDynamoDBDocumentClientshould get a matching@aws-sdk/client-dynamodband@aws-sdk/lib-dynamodbpair.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_modulesbefore/var/runtime/node_modulesonNODE_PATH, a function using the DocumentClient resolves:@aws-sdk/client-dynamodbfrom the layer@aws-sdk/lib-dynamodbfrom the runtimeWhen those two land on opposite sides of the AWS SDK for JavaScript 3.928.0 boundary — which moved
serializerMiddlewareregistration from per-command to per-client — the mismatch fails at runtime: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:49shipsclient-dynamodb@3.1084.0, which is newer than the runtime'slib-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
Steps to Reproduce
@aws-sdk/client-dynamodbor@aws-sdk/lib-dynamodb, so both come from outside/var/task.@aws-sdk/client-dynamodbis older than 3.928.0 (e.g. v1 layerAWSLambdaPowertoolsTypeScript:27, which ships 3.454.0).DynamoDBDocumentClient.from(new DynamoDBClient({}))and send any command.serializerMiddleware is not founderror.To confirm the resolution split on any layer version:
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-sdkpackages, includinglib-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.xandnodejs24.x, CJS and ESM: all utilities (Logger, Metrics, Tracer includingcaptureAWSv3Client, Parameters, Idempotency, Parser, Batch) work with no@aws-sdk/*in the layer. Note that@smithy/*andzodare not provided by the runtime and must remain in the layer —@smithy/service-error-classificationis a runtime dependency ofaws-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.0with 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. Runtimenodejs22.x.@aws-sdk/client-dynamodbresolves from/opt/nodejs@3.454.0/var/runtime@3.1049.0@aws-sdk/lib-dynamodbresolves from/var/runtime@3.1049.0/var/runtime@3.1049.0The 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.mdcurrently tells customers three times (lines 157, 226, 255 — CDK, SAM and Serverless Framework examples) to exclude@aws-sdk/*from their bundle: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/taskis 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.mdneeds 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 ...", ] }