Repository navigation
Lambda Response Streaming never completes due to injected SSM refresh setInterval #2981
Description
Environment information
System:
OS: macOS 15.6.1
CPU: (10) arm64 Apple M2 Pro
Memory: 225.67 MB / 16.00 GB
Shell: /bin/zsh
Binaries:
Node: 22.13.1 - ~/.nvm/versions/node/v22.13.1/bin/node
Yarn: 1.22.22 - /opt/homebrew/bin/yarn
npm: 10.9.2 - ~/.nvm/versions/node/v22.13.1/bin/npm
pnpm: undefined - undefined
NPM Packages:
@aws-amplify/auth-construct: 1.8.1
@aws-amplify/backend: 1.16.1
@aws-amplify/backend-ai: Not Found
@aws-amplify/backend-auth: 1.7.1
@aws-amplify/backend-cli: 1.7.2
@aws-amplify/backend-data: 1.6.1
@aws-amplify/backend-deployer: 2.1.1
@aws-amplify/backend-function: 1.14.1
@aws-amplify/backend-output-schemas: 1.6.0
@aws-amplify/backend-output-storage: 1.3.1
@aws-amplify/backend-secret: 1.4.0
@aws-amplify/backend-storage: 1.4.1
@aws-amplify/cli-core: 2.1.1
@aws-amplify/client-config: 1.7.0
@aws-amplify/data-construct: 1.16.1
@aws-amplify/data-schema: 1.21.0
@aws-amplify/deployed-backend-client: 1.7.0
@aws-amplify/form-generator: 1.2.1
@aws-amplify/model-generator: 1.2.0
@aws-amplify/platform-core: 1.9.0
@aws-amplify/plugin-types: 1.10.1
@aws-amplify/sandbox: 2.1.2
@aws-amplify/schema-generator: 1.4.0
@aws-cdk/toolkit-lib: 0.3.2
aws-amplify: 6.14.4
aws-cdk-lib: 2.189.1
typescript: 5.8.3
npm warn exec The following package was not found and will be installed: cdk@2.1029.0
No AWS environment variables
No CDK environment variables
Describe the bug
When deploying a Lambda with Amplify Gen 2 that uses awslambda.streamifyResponse for response streaming, the function never terminates. Invocation times out even though responseStream.end() has been called.
Inspection of the transpiled bundle shows Amplify injecting SSM environment variable resolution logic that sets up a recurring setInterval to refresh SSM parameters every 60 seconds. This interval is not unref()’d and pins the Node.js event loop, preventing the Lambda runtime from completing.
Setting context.callbackWaitsForEmptyEventLoop = false allows the function to exit, but that only hides the root issue: it causes Lambda to freeze open handles across warm invocations, creating the risk of timers, sockets, or subscriptions leaking between requests.
Reproduction steps
- Create a new Amplify Gen 2 backend.
- Create a (simplified) Lambda function handler:
// handler.ts
export const handler = awslambda.streamifyResponse(async (_event, stream, _context) => {
stream.write("Hello ");
stream.write("world");
stream.end();
});- Define the Amplify function with a secret:
// resource.ts
import { defineFunction, secret } from '@aws-amplify/backend';
const streamFunction = defineFunction({
name: 'StreamFunction',
runtime: 22,
entry: './handler.ts',
timeoutSeconds: 60,
environment: {
SOME_SECRET: secret('SOME_SECRET'),
},
});- Attach a Function URL and enable Invoke mode
RESPONSE_STREAM.
// backend.ts
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { streamFunction } from './resource';
const backend = defineBackend({
...,
streamFunction
});
backend.streamFunction.resources.lambda.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
invokeMode: lambda.InvokeMode.RESPONSE_STREAM,
});- Deploy.
- Invoke the Function URL, e.g.
curl https://342dqyq5y5d3yq5gd35df3tg.lambda-url.eu-west-1.on.aws/. - Observe the Lambda runs until timeout and never completes (in CloudWatch Logs), even though the client receives a full response.
Inspecting the built index.mjs shows Amplify injecting:
const SSM_PARAMETER_REFRESH_MS = 1000 * 60;
setInterval(async () => {
try { await internalAmplifyFunctionResolveSsmParams(); }
catch (error) { console.debug(error); }
}, SSM_PARAMETER_REFRESH_MS);This timer keeps the event loop alive indefinitely.
Expected behavior
A streaming Lambda should terminate after stream.end() once response transmission is complete, without requiring callbackWaitsForEmptyEventLoop = false.
Activity
pahud commentedon Sep 26, 2025
Hi @eriktim,
Thank you for this bug report with clear reproduction steps and root cause analysis.
The problem seems to be in the invoke_ssm_shim.ts file where the
setIntervalis not unreferenced, causing it to pin the Node.js event loop and prevent Lambda from detecting completion.Possible Root Cause:
The SSM parameter refresh interval keeps the event loop active indefinitely because it's not marked as unreferenced. This prevents streaming Lambda functions from terminating naturally after
stream.end().Workaround:
As you mentioned, setting
context.callbackWaitsForEmptyEventLoop = falseworks but isn't ideal as it can cause resource leaks across warm invocations.