https://github.com/mikkopiu/lambda-deadline-middleware
Zero-dependency AWS SDK v3 middleware that automatically propagates Lambda execution deadlines to outgoing SDK calls via AbortController-based timeouts
https://github.com/mikkopiu/lambda-deadline-middleware
aws aws-lambda aws-lambda-node aws-sdk aws-sdk-v3 deadline middleware nodejs smithy timeout typescript
Last synced: 14 days ago
JSON representation
Zero-dependency AWS SDK v3 middleware that automatically propagates Lambda execution deadlines to outgoing SDK calls via AbortController-based timeouts
- Host: GitHub
- URL: https://github.com/mikkopiu/lambda-deadline-middleware
- Owner: mikkopiu
- License: mit
- Created: 2026-06-12T09:52:33.000Z (17 days ago)
- Default Branch: main
- Last Pushed: 2026-06-12T16:33:03.000Z (17 days ago)
- Last Synced: 2026-06-12T17:26:55.831Z (17 days ago)
- Topics: aws, aws-lambda, aws-lambda-node, aws-sdk, aws-sdk-v3, deadline, middleware, nodejs, smithy, timeout, typescript
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/lambda-deadline-middleware
- Size: 112 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
README
# lambda-deadline-middleware
Zero-dependency AWS SDK v3 middleware that automatically propagates Lambda execution deadlines to outgoing SDK calls via
`AbortController`-based timeouts.
When an AWS SDK call hangs inside a Lambda function, the runtime terminates the process at the configured timeout,
destroying in-flight OpenTelemetry/X-Ray spans without export. This library prevents that by computing per-request
deadlines from the Lambda's remaining execution time and aborting requests before the hard timeout fires.
## Features
- Automatic deadline propagation, no manual timeout configuration per call
- Fresh deadline per retry: each SDK retry attempt uses _current_ remaining time
- Signal composition: preserves caller-provided `AbortSignal` via `AbortSignal.any()`
- Zero runtime dependencies (`@smithy/types` is compile-time only)
- Complete no-op when no Lambda context is available
- Optional OpenTelemetry span events on deadline aborts (detected dynamically)
- Branded types prevent millisecond/buffer interchange at compile time
## Requirements
- Node.js ≥ 24
- AWS SDK v3 (built against `@smithy/types` ≥ 3.0.0)
## Installation
```bash
pnpm add lambda-deadline-middleware
```
## Usage
Setup requires two pieces:
1. **Wrap your handler** with `withLambdaDeadline`. This stores the Lambda `context` (specifically
`getRemainingTimeInMillis()`) in `AsyncLocalStorage` so the SDK middleware can read it. The SDK middleware stack has
no access to the Lambda context on its own.
2. **Register the middleware** on each SDK client via the standard `middlewareStack.use()` pattern.
```typescript
import { withLambdaDeadline, deadlineMiddleware } from "lambda-deadline-middleware";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const dynamodb = new DynamoDBClient({});
dynamodb.middlewareStack.use(deadlineMiddleware());
export const handler = withLambdaDeadline(async (event, context) => {
const result = await dynamodb.send(
new GetItemCommand({
/* ... */
}),
);
return { statusCode: 200, body: JSON.stringify(result) };
});
```
Every SDK call through `dynamodb` now receives a timeout derived from the Lambda's remaining execution time minus a
configurable flush buffer (default: 1000ms).
## How It Works
```mermaid
flowchart LR
subgraph Lambda Invocation
direction LR
A[Lambda Runtime] --> B[withLambdaDeadline]
B --> C[Your Handler]
C --> D[SDK .send]
end
subgraph Per Attempt
direction LR
D --> E[Deadline Middleware]
E -->|getRemainingTimeInMillis\nminus flush buffer| F[AbortController\n+ setTimeout]
F --> G[HTTP Request]
end
style E fill:#f9a825,stroke:#f57f17
style B fill:#66bb6a,stroke:#2e7d32
```
`withLambdaDeadline` stores the Lambda context in `AsyncLocalStorage`. The deadline middleware reads it on every attempt
(including retries), computes a fresh timeout, and attaches an `AbortSignal` to the outgoing HTTP request.
## Configuration
### Flush Buffer
The flush buffer is subtracted from the remaining Lambda time to leave room for telemetry export and error handling:
```typescript
// Default: 1000ms
dynamodb.middlewareStack.use(deadlineMiddleware());
// Custom: 500ms
dynamodb.middlewareStack.use(deadlineMiddleware({ flushBufferMs: 500 }));
```
### Telemetry
If `@opentelemetry/api` is installed, span events are emitted on deadline aborts. Disable with:
```typescript
dynamodb.middlewareStack.use(deadlineMiddleware({ telemetryEnabled: false }));
```
## Error Handling
When remaining time is less than or equal to the flush buffer, the middleware throws `DeadlineExceededError` immediately
without dispatching an HTTP request.
```typescript
import { isDeadlineExceeded } from "lambda-deadline-middleware";
try {
await dynamodb.send(
new GetItemCommand({
/* ... */
}),
);
} catch (error) {
if (isDeadlineExceeded(error)) {
console.log(`Deadline exceeded: ${error.deadlineMs}ms`);
console.log(`Remaining time was: ${error.remainingMs}ms`);
}
throw error;
}
```
## Signal Composition
If you pass an `AbortSignal` to a request, the middleware composes both signals:
```typescript
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await dynamodb.send(
new GetItemCommand({
/* ... */
}),
{
abortSignal: controller.signal,
},
);
```
## API Reference
### `withLambdaDeadline(handler, options?)`
Wraps a Lambda handler to store the Lambda context in `AsyncLocalStorage`. Required for the middleware to access
`getRemainingTimeInMillis()`.
```typescript
function withLambdaDeadline(
handler: (event: TEvent, context: LambdaContextLike) => Promise,
options?: DeadlineOptions,
): (event: TEvent, context: LambdaContextLike) => Promise;
```
### `deadlineMiddleware(options?)`
Returns a `Pluggable` for `client.middlewareStack.use()`.
```typescript
function deadlineMiddleware(options?: DeadlineOptions): Pluggable;
```
### `getRemainingTimeInMillis()`
Accessor for the current Lambda's remaining execution time. Returns `undefined` outside a Lambda context.
```typescript
function getRemainingTimeInMillis(): number | undefined;
```
### `isDeadlineExceeded(error)`
Type guard for deadline-triggered abort errors.
```typescript
function isDeadlineExceeded(error: unknown): error is DeadlineExceededError;
```
### `DeadlineExceededError`
```typescript
class DeadlineExceededError extends Error {
readonly name: "DeadlineExceededError";
readonly deadlineMs: Milliseconds;
readonly flushBufferMs: FlushBufferMs;
readonly remainingMs: Milliseconds;
}
```
### `DeadlineOptions`
```typescript
interface DeadlineOptions {
readonly flushBufferMs?: number; // Default: 1000
readonly telemetryEnabled?: boolean; // Default: true
}
```
### Types
| Type | Description |
| --------------------- | ---------------------------------------------------------------------------- |
| `Milliseconds` | Branded number representing a duration in ms |
| `FlushBufferMs` | Branded number for the flush buffer |
| `RequestDeadlineMs` | Branded number for a computed deadline |
| `DeadlineComputation` | Discriminated union: `"deadline"` \| `"insufficient-time"` \| `"no-context"` |
| `LambdaContextLike` | Minimal interface: `{ getRemainingTimeInMillis?(): number }` |
## Reporting Bugs
Found a bug? Please open a [GitHub Issue](https://github.com/mikkopiu/lambda-deadline-middleware/issues/new) with:
- Your Node.js version and AWS SDK version
- A minimal code snippet reproducing the problem
- Expected vs actual behavior
For security vulnerabilities, see [SECURITY.md](SECURITY.md) instead.
## License
[MIT](LICENSE)