This PR builds on
https://github.com/awslabs/swift-aws-lambda-runtime/pull/629 to add
convenience structs (Handlers and Adapters) that are `Sendable`
**Changes**
- **Added Sendable adapter types**: Implemented `ClosureHandlerSendable`
- a thread-safe version of existing closure handler that enforces
`Sendable` conformance for concurrent execution environments - and added
conditional conformance to `Sendable` for other Adapters when the
Handler is `Sendable`
- **Enhanced handler protocols for concurrency**: Extended handler
protocols to support `Sendable` constraints and concurrent response
writing through `LambdaResponseStreamWriter & Sendable`, enabling safe
multi-threaded invocation processing
- **Created comprehensive Lambda Managed Instances examples**: Built
three demonstration functions showcasing concurrent execution
capabilities, streaming responses, and background processing patterns
specific to the new managed instances deployment model
**Context**
Lambda Managed Instances support multi-concurrent invocations where
multiple invocations execute simultaneously within the same execution
environment. The runtime now detects the configured concurrency level
and launches the appropriate number of RICs to handle concurrent
requests efficiently.
When `AWS_LAMBDA_MAX_CONCURRENCY` is 1 or unset, the runtime maintains
the existing single-threaded behaviour for optimal performance on
traditional Lambda deployments.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
AWS launched [Lambda Managed
Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html),
i.e Lambda functions running on EC2 instances.
This comes with [a major change in the programming
model](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html#lambda-managed-instances-concurrency-model)
as function handlers are now allowed to run concurrently on the same
machine (multiple in flight events being processed in parallel in the
same execution environment). The maximum concurrency per runtime
environment is controlled by the user.
This PR adds support for running multiple Runtime Interface Clients
(RICs) concurrently when deployed on Lambda Managed Instances, enabling
the runtime to handle multiple invocations simultaneously within a
single execution environment.
This PR is a followup to
https://github.com/awslabs/swift-aws-lambda-runtime/pull/617 which used
another approach to support Lambda Managed Instances by changing the
public API and requiring that all handlers must conform to `Sendable`.
The original PR was closed as we agreed that only a fraction of the
Lambda functions will be deployed on EC2 and it was not worth adding a
`Sendable` requirement for all.
**Changes**
- **Introduced thread-safe LambdaManagedRuntime**: Created new
Sendable-conforming runtime class that supports concurrent handler
execution with atomic guards to prevent multiple runtime instances and
thread-safe handler requirements (`Handler: StreamingLambdaHandler &
Sendable`)
- **Implemented ServiceLifecycle integration**: Added managed runtime
support for structured concurrency lifecycle management, allowing proper
startup/shutdown coordination in multi-concurrent environments
This PR contains only changes to the core runtime, convenience
functions, handlers, adapters, and a comprehensive example will be added
in a follow up PR.
**Context**
Lambda Managed Instances support multi-concurrent invocations where
multiple invocations execute simultaneously within the same execution
environment. The runtime now detects the configured concurrency level
and launches the appropriate number of RICs to handle concurrent
requests efficiently.
When `AWS_LAMBDA_MAX_CONCURRENCY` is 1 or unset, the runtime maintains
the existing single-threaded behaviour for optimal performance on
traditional Lambda deployments.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
On fast machines, the local Lambda server crashes with:
```
Fatal error: Deinited NIOAsyncWriter without calling finish()
```
This occurs in `NIOAsyncChannelHandler.channelActive()` when child
connection channels are created.
## Root Cause
This is a known issue with NIO's async server channel API (see
[swift-nio#2637](https://github.com/apple/swift-nio/issues/2637)).
**The fundamental problem:**
1. The async `bind()` API creates `NIOAsyncChannel` instances for
incoming connections
2. These channels are yielded through an async stream to the server loop
3. When the serving task is cancelled (or completes), the async stream
iteration stops
4. Any channels that were accepted but not yet read from the stream are
dropped
5. These unread channels never have `executeThenClose()` called on them
6. Their `NIOAsyncWriter` is deallocated without `finish()` being called
→ fatal error
**Why graceful shutdown doesn't help:**
Even closing the server channel gracefully doesn't eliminate the race -
there's a timing window where:
- A connection is accepted and queued in the async stream
- The server task is cancelled or completes
- The queued channel is never read and gets dropped
IMHO, this is an inherent limitation of the `async bind()` API when
combined with task cancellation.
## Solution
I stopped using the `async bind()` API entirely. Instead, I use the
traditional callback-based `childChannelInitializer`:
1. Create `NIOAsyncChannel` directly in `childChannelInitializer`
(synchronous context)
2. Immediately spawn a `Task.detached` to handle the connection
3. Each connection is handled independently, not through a cancellable
async stream
4. Detached tasks are not affected by task group cancellation
5. Every channel has `executeThenClose()` called immediately, preventing
the writer from being dropped
This approach avoids the async stream entirely, eliminating the race
condition.
## Changes
- Replaced `async bind()` with traditional `childChannelInitializer`
- Each connection spawns a `Task.detached` that immediately calls
`executeThenClose()`
- Removed the connection iteration loop (no longer needed)
- Server task now simply waits for the channel to close
- Simplified shutdown logic since there's no async stream to drain
## Trade-offs
- Uses `Task.detached` (unstructured concurrency) to bridge NIO's
event-loop world with Swift concurrency
- This is necessary until NIO provides a new bootstrap API that properly
handles cancellation
- Each connection is handled independently rather than through
structured concurrency
## Testing
Tested on fast machines where the race condition was reliably
reproducible. The crash no longer occurs.
## References
- [swift-nio#2637](https://github.com/apple/swift-nio/issues/2637) -
Known issue with async server channels and cancellation
- [Comment from NIO
maintainer](https://github.com/apple/swift-nio/issues/2637#issuecomment-1921317577)
- Recommends avoiding cancellation or using callback-based API
Fixes#635
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
This PR fixes a race condition in `LambdaRuntimeClient` that causes a
fatal crash when an old channel's `closeFuture` callback fires after a
new connection has been established. The fix adds proper channel
lifecycle tracking and replaces the fatal error with graceful handling.
## Problem
**Crash Location**: `LambdaRuntimeClient.swift:270` in `channelClosed()`
**Error Message**:
```
Fatal error: Invalid state: connected(SocketChannel { ... }), closed
```
**Root Cause**: Race condition where:
1. An old channel's `closeFuture` callback fires
2. AFTER a new connection has been established (`connectionState =
.connected`)
3. BUT `closingState` is `.closed` from a previous close operation
4. The code asserted this was impossible and crashed with `fatalError`
This can occur when:
- Network conditions cause delayed channel cleanup
- Connection is recycled quickly (old channel still closing while new
one connects)
- Timing issues between channel close callbacks and new connection
establishment
## Solution
### Key Changes
1. **Added channel identity tracking**:
```swift
private var channelsBeingClosed: Set<ObjectIdentifier> = []
```
Tracks which channels are in the process of closing to distinguish old
channels from the current one.
2. **Enhanced `connectionWillClose()`**:
- Marks channels as "being closed" using `ObjectIdentifier`
- Adds logging when old channels close while new connection is active
3. **Rewrote `channelClosed()` with defensive logic**:
- **Early return for tracked old channels**: Handles them gracefully
without affecting current connection
- **Replaced `fatalError` with warning log**: The `(_, .closed)` case
now logs a warning instead of crashing
- **Channel identity checks**: Only transitions state if the closing
channel is the CURRENT channel
- **Removed unconditional state change**: Previously set
`connectionState = .disconnected` for ANY channel close, now only for
the current channel
### Why This Fixes the Bug
The fix addresses the race condition by:
- Distinguishing between "current channel closing" vs "old channel
closing"
- Handling old channel closes gracefully without crashing or corrupting
state
- Not overwriting connection state when old channels close
- Providing visibility through logging when the race condition occurs
## Changes
### Modified Files
- **Sources/AWSLambdaRuntime/HTTPClient/LambdaRuntimeClient.swift**
- Added `channelsBeingClosed: Set<ObjectIdentifier>` property
- Enhanced `connectionWillClose()` with channel tracking
- Rewrote `channelClosed()` with defensive logic and identity checks
- Replaced `fatalError` with warning log for unexpected states
- Removed unconditional state change in `closeFuture` callback
**Lines Changed**: ~150 lines modified/added
**Backward Compatibility**: ✅ Fully compatible, no API changes
## Testing
### ✅ All Existing Tests Pass
```bash
swift test
# Result: 91 tests passed in 14 suites
```
All original functionality is preserved with no regressions.
### ⚠️ Note on Test Coverage
While we cannot reproduce the exact race condition from bug #624 in a
deterministic test (it requires specific network timing), the fix:
- Is logically sound for the described race condition
- Improves defensive programming around channel lifecycle
- Replaces a fatal crash with graceful handling + logging
- Should prevent the crash by properly tracking channel identity
## Related Issues
Fixes#624
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
# Fix test hangs caused by Pool cancellation race conditions
## Summary
This PR fixes two related race conditions in
`Lambda+LocalServer+Pool.swift` that were causing the test suite to hang
approximately 10% of the time.
## Problem
The test suite exhibited intermittent hangs (~10% frequency) due to two
bugs in the Pool implementation:
1. **Individual task cancellation bug**: When one task waiting for a
specific `requestId` was cancelled, the cancellation handler would
incorrectly cancel ALL waiting tasks instead of just the cancelled one.
2. **Server shutdown hang**: When the server shut down, waiting
continuations in the pools were never cancelled, causing handlers to
wait indefinitely for responses that would never arrive.
## Root Causes
### Root Cause #1: Cancellation Handler Removes ALL Continuations
The `onCancel` handler in `Pool._next()` was removing all continuations
from the `waitingForSpecific` dictionary when any single task was
cancelled:
```swift
onCancel: {
// BUG: Removes ALL continuations, not just the cancelled task's
for continuation in state.waitingForSpecific.values {
toCancel.append(continuation)
}
state.waitingForSpecific.removeAll()
}
```
This caused unrelated concurrent invocations to fail with
`CancellationError` when one client cancelled their request.
### Root Cause #2: No Pool Cleanup During Server Shutdown
When the server shut down (e.g., test completes), the task group was
cancelled but the pools' waiting continuations were never notified. The
`/invoke` endpoint handlers would continue waiting for responses that
would never arrive because the Lambda function had stopped.
## Solution
### Fix#1: Only Remove Specific Continuation on Cancellation
Modified the cancellation handler to only remove the continuation for
the specific cancelled task:
```swift
onCancel: {
// Only remove THIS task's continuation
let continuationToCancel = self.lock.withLock { state -> CheckedContinuation<T, any Error>? in
if let requestId = requestId {
return state.waitingForSpecific.removeValue(forKey: requestId)
} else {
let cont = state.waitingForAny
state.waitingForAny = nil
return cont
}
}
continuationToCancel?.resume(throwing: CancellationError())
}
```
### Fix#2: Add Pool Cleanup During Server Shutdown
Added `cancelAll()` method to the Pool class and call it during server
shutdown:
```swift
func cancelAll() {
let continuationsToCancel = self.lock.withLock { state -> [CheckedContinuation<T, any Error>] in
var toCancel: [CheckedContinuation<T, any Error>] = []
if let continuation = state.waitingForAny {
toCancel.append(continuation)
state.waitingForAny = nil
}
for continuation in state.waitingForSpecific.values {
toCancel.append(continuation)
}
state.waitingForSpecific.removeAll()
return toCancel
}
for continuation in continuationsToCancel {
continuation.resume(throwing: CancellationError())
}
}
```
Called during server shutdown:
```swift
let serverOrHandlerResult1 = await group.next()!
group.cancelAll()
// Cancel all waiting continuations in the pools to prevent hangs
server.invocationPool.cancelAll()
server.responsePool.cancelAll()
```
## Changes
### Modified Files
- **Sources/AWSLambdaRuntime/HTTPServer/Lambda+LocalServer+Pool.swift**
- Fixed cancellation handler in `_next()` to only remove specific
continuation
- Added `cancelAll()` method for server shutdown cleanup
- **Sources/AWSLambdaRuntime/HTTPServer/Lambda+LocalServer.swift**
- Call `cancelAll()` on both pools during server shutdown
### New Files
- **Tests/AWSLambdaRuntimeTests/LocalServerPoolCancellationTests.swift**
- Added comprehensive test suite with 3 tests
- `testCancellationOnlyAffectsOwnTask`: Verifies only the cancelled task
receives CancellationError
- `testConcurrentInvocationsWithCancellation`: Tests real-world scenario
with 5 concurrent invocations
- `testFIFOModeCancellation`: Ensures FIFO mode cancellation works
correctly
## Testing
### Before Fix
- Test suite hung ~10% of the time
- When 1 task was cancelled, all 5 concurrent tasks received
`CancellationError`
- Streaming tests would occasionally hang during shutdown
### After Fix
- All 91 tests pass consistently without hangs
- When 1 task is cancelled, only that specific task receives
`CancellationError`
- Other tasks continue waiting normally
- Server shutdown properly cleans up all waiting continuations
- Multiple consecutive test runs confirm stability
### Test Coverage
The new test suite reproduces both bugs and verifies the fixes:
1. **testCancellationOnlyAffectsOwnTask**: Creates 3 tasks waiting for
different requestIds, cancels only one, and verifies the others are not
affected
2. **testConcurrentInvocationsWithCancellation**: Simulates 5 concurrent
invocations with one cancellation
3. **testFIFOModeCancellation**: Tests FIFO mode to ensure it still
works correctly
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
refcator to isolate the code in `startRuntimeInterfaceClient` and
`startLocalServer` functions
Also change the name of the global variable `_isRunning` to
`_isLambdaRuntimeRunning`
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
Rename Lambda+JSON file to LambdaRuntime+JSON, to prepare for furture
support of Lambda Managed Runtime
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
When using `trace` log level the runtime diplays 1Kb of the input
payload to help debugging cases where the JSON decoding fails. Most of
the type, this 1 Kb trace is not enough to understand what part of the
incoming JSON fails to decode.
This PR raises the limit to 6Mb to be actually useful.
This PR removes `Sendable` requirement on `StreamingClosureHandler `.
While analyzing code for the upcoming support of Lambda Managed Runtime,
I found out that `StreamingClosureHandler` has `Sendable` requirement,
which is not necessary.
Removing requirement doesn't require a major speed bump as the change is
compatible with existing code.
<!--- Provide a general summary of your changes in the Title above -->
Fix after release of swift-log 1.8.0
## Issue \#
<!--- If it fixes an issue, please link to the issue here -->
## Description of changes
<!--- Why is this change required? What problem does it solve? -->
## New/existing dependencies impact assessment, if applicable
<!--- No new dependencies were added to this change. -->
<!--- If any dependency was added / modified / removed,
THIRD-PARTY-LICENSES must be updated accordingly. -->
## Conventional Commits
<!--- Please use conventional commits to let us know what kind of change
this is.-->
<!--- More info can be found here:
https://www.conventionalcommits.org/en/v1.0.0/-->
By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 license.
## Description
Adds documentation for Docker credential store errors that users may
encounter when running `swift package archive
--allow-network-connections docker`.
## Changes
- Added a note in `readme.md` explaining how to resolve Docker
credential store errors
- Added a note in `Sources/AWSLambdaRuntime/Docs.docc/Deployment.md`
with the same guidance
## Problem
Users may encounter authentication errors when the Swift package plugin
attempts to pull Docker images, particularly when Docker is configured
with a credential store (e.g., `docker-credential-desktop`). This issue
is documented in #609.
## Solution
The documentation now provides two workarounds:
1. Remove the `credsStore` entry from `~/.docker/config.json`
2. Use the `--disable-sandbox` flag with the archive command
Both notes link to issue #609 for additional context and discussion.
## Related Issue
Fixes#609
Address https://github.com/awslabs/swift-aws-lambda-runtime/issues/605
NEW Lambda Tenant isolation capability:
https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html
# Add Support for Lambda Tenant Isolation Mode
## Summary
This PR adds support for AWS Lambda's tenant isolation mode to the Swift
AWS Lambda Runtime, enabling developers to build multi-tenant
applications with strict execution environment isolation per tenant.
## Changes
### Runtime Support
- Added `tenantID` property to `LambdaContext` to expose the tenant
identifier
- Extended `InvocationMetadata` to capture the
`Lambda-Runtime-Aws-Tenant-Id` header
- Added `AmazonHeaders.tenantID` constant for the tenant ID header
- Added trace logging for invocation headers to aid debugging
### New Example: MultiTenant
A complete working example demonstrating tenant isolation mode:
- **Request tracking system** that maintains separate counters and
histories per tenant
- **Actor-based storage** (`TenantDataStore`) for thread-safe tenant
data management
- **Immutable data structures** (`TenantData`) following Swift best
practices
- **API Gateway integration** with tenant ID passed via query parameter
- **SAM template** configured with `TenancyConfig.TenantIsolationMode:
PER_TENANT`
- **Comprehensive documentation** covering architecture, deployment,
testing, and best practices
### Testing
- Added unit test for tenant ID extraction from invocation headers
- Integrated MultiTenant example into CI/CD pipeline
### Documentation
The example includes detailed documentation on:
- When to use tenant isolation (user code execution, sensitive data
processing)
- How tenant isolation works (dedicated environments, no cross-tenant
reuse)
- Concurrency limits and scaling considerations
- Pricing implications
- Security best practices
- CloudWatch monitoring with tenant dimensions
## Files Changed
- `Sources/AWSLambdaRuntime/LambdaContext.swift` - Added tenantID
property
- `Sources/AWSLambdaRuntime/ControlPlaneRequest.swift` - Capture tenant
ID from headers
- `Sources/AWSLambdaRuntime/Utils.swift` - Added tenantID header
constant
- `Sources/AWSLambdaRuntime/Lambda.swift` - Pass tenant ID to context
- `Sources/AWSLambdaRuntime/LambdaRuntimeClient+ChannelHandler.swift` -
Added trace logging
- `Tests/AWSLambdaRuntimeTests/InvocationTests.swift` - Added tenant ID
test
- `Examples/MultiTenant/*` - New complete example with SAM template
- `.github/workflows/pull_request.yml` - Added MultiTenant to CI
pipeline
## Testing Instructions
1. Build and deploy the example:
bash
cd Examples/MultiTenant
swift package archive --allow-network-connections docker
sam deploy --guided
2. Test with different tenants:
bash
curl
"https://<api-id>.execute-api.<region>.amazonaws.com/Prod?tenant-id=
alice"
curl
"https://<api-id>.execute-api.<region>.amazonaws.com/Prod?tenant-id=
bob"
3. Verify isolation by checking that each tenant maintains separate
request counts
## Related Documentation
- [AWS Lambda Tenant
Isolation](https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html)
- [AWS Blog: Streamlined Multi-Tenant Application
Development](https://aws.amazon.com/blogs/aws/streamlined-multi-tenant-application-development-with-tenant-isolation-mode-in-aws-lambda/)
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
Co-authored-by: Tim Condon <0xTim@users.noreply.github.com>
By default, the script started the MockServer on port 7000, which is
used by AirPlay on macOS.
This caused conflicts.
I also made a minor change on the logging to avoir reporting an error
when the server closes the connection.
Closing
https://github.com/swift-server/swift-aws-lambda-runtime/issues/584
The LocalServer now queues concurrent `POST /invoke` requests from
testing client applications and ensures that the requests are delivered
to the Lambda Runtime one by one, just like the AWS Lambda Runtime
environment does.
The `Pool` has now two modes : pure FIFO (one element get exactly one
`next()`) and one mode where multiple elements can get pushed and
multiple `next(for requestId:String)` can be called concurrently.
The two modes are needed because invocations are 1:1 (one `POST /invoke`
is always by one matching `GET /next`) but responses are n:n (a response
can have multiple chunks and concurrent invocations can trigger multiple
`next(for requestId: String)`
I made a couple of additional changes while working on this PR
- I moved the `Pool` code in a separate file for improved readability
- I removed an instance of `DispatchTime` that was hiding in the code,
unnoticed until today
- I removed the `async` requirement on `Pool.push(_)` function. This was
not required (thank you @t089 for having reported this)
- I removed the `fatalError()` that was in the `Pool` implementation.
The pool now throws an error when `next()` is invoked concurrently,
making it easier to test.
- I added extensive unit tests to validate the Pool behavior
- I added a test to verify that a rapid succession of client invocations
are correctly queued and return no error
- I moved a `continuation(resume:)` outside of a lock. Generally
speaking, it's a bad idea to resume continuation while owning a lock. I
suspect this is causing a error during test execution when we spawn and
tear down mutliple `Task` very quickly. In some rare occasions, the test
was failing with an invalid assertion in NIO :
`NIOCore/NIOAsyncWriter.swift:177: Fatal error: Deinited NIOAsyncWriter
without calling finish()`
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Adjust notice, security reporting, code of conduct, contribution
process to the standard AWS documents
- Adjust GitHub issue templates to AWS standard ones.
- Adjust the license header in all source files
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
Fix errors in the CI
The script that checks the presence of `libFoundation` in the binary
started to fail.
I can't think about a recent change that would cause this.
This PR change the test script to use the `HelloWorld` example instead
of `APIGAtewayV2`
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
The Local HTTP Server (used when testing) used to block after one
invocation of a streaming lambda function. Now you can invoke multiple
times your streaming function without having to restart the local HTTP
server.
### Motivation:
Bug https://github.com/swift-server/swift-aws-lambda-runtime/issues/588
### Modifications:
The flow to respond to streaming and non-streaming requests are
different. In the streaming request flow, we forgot to send an 202
accept response to the lambda runtime client after it posted the end
chunck of the response (in other words, `POST /response` never received
an HTTP 202 response.) This caused the Lambda Runtime to hang and never
issue the next `GET /next `request.
### Result:
You can now send multiple invocations to your streaming lambda.
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
_Add convenience initializer for `LambdaRuntime` to accept
`LambdaHandler` instances with `Void` output_
### Motivation:
Following PR #581, which added convenience initializers for
`LambdaRuntime` to accept `LambdaHandler` instances with `Encodable`
outputs, there was still a gap for handlers that return `Void`. This is
useful, for example, for AWS event sources like SQS, where the
`AWSLambdaEvents` package provides `SQSEvent` but there's no
corresponding output type - handlers simply process messages and return
`Void`.
Without this initializer, developers had to manually wrap their handlers
with `LambdaCodableAdapter` and `LambdaHandlerAdapter`, which was
verbose and inconsistent with the new API (similar to what is described
in #581)
### Modifications:
1. Added a new `init(decoder:handler:)` convenience initializer to
`LambdaCodableAdapter` in `Lambda+JSON.swift` that accepts a
`JSONDecoder` for handlers with `Void` output.
2. Added a new convenience initializer to `LambdaRuntime` that accepts a
`LambdaHandler` instance with `Void` output directly, handling the
wrapping internally.
3. Updated documentation comments to distinguish between handlers with
`Void` and `Encodable` outputs.
### Result:
Developers can now initialize `LambdaRuntime` with handlers that return
`Void` using a clean, direct API. This is especially useful for event
sources like SQS:
```swift
import AWSLambdaEvents
struct MySQSHandler: LambdaHandler {
func handle(_ event: SQSEvent, context: LambdaContext) async throws {
// Process SQS messages
}
}
let runtime = LambdaRuntime(lambdaHandler: MySQSHandler())
```
This provides API completeness, matching the convenience initializers
for handlers with `Encodable` outputs, and delivers better ergonomics
for common serverless patterns.
### Motivation:
Fix for Issue
[#580](https://github.com/swift-server/swift-aws-lambda-runtime/issues/580),
by making it so that the `errorType` in failed requests will be the type
of the error entity, rather than a hardcoded string of `FunctionError`.
This allows orchestration within step functions that perform retry/catch
logic based on different error output types.
### Modifications:
At a high level, the issue is that swift-aws-lambda-runtime, when an
error is thrown, outputs the errorType as hardcoded to FunctionError.
You can see that
[here](https://github.com/swift-server/swift-aws-lambda-runtime/blob/main/Sources/AWSLambdaRuntime/LambdaRuntimeClient%2BChannelHandler.swift#L337):
```
let errorResponse = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)")
```
This PR changes this for all cases to output the type of the error,
rather than the hardcoded string:
```
let errorResponse = ErrorResponse(errorType: "\(type(of: error))", errorMessage: "\(error)")
```
Now, I will show 2 examples with this solution:
```
let runtime = LambdaRuntime {
(event: Input, context: LambdaContext) in
enum MyTestErrorType: Error {
case testError
}
throw MyTestErrorType.testError
}
// outputs {"errorType":"MyTestErrorType","errorMessage":"testError"}
```
```
let dynamoDB: DynamoDB = DynamoDB(client: .init())
let runtime = LambdaRuntime {
(event: Input, context: LambdaContext) in
let _ = try await dynamoDB.putItem(DynamoDB.PutItemInput(item: [:], tableName: ""))
return Output()
}
// outputs {"errorType":"AWSClientError","errorMessage":"ValidationError: Length of PutItemInput.tableName (0) is less than minimum allowed value 1."}
```
_Add convenience initializer for `LambdaRuntime` to accept
`LambdaHandler` instances directly_
### Motivation:
When using the Swift AWS Lambda Runtime with custom handler types that
conform to `LambdaHandler`, developers previously had two options to
initialize `LambdaRuntime`:
1. Manually wrap their handler with `LambdaCodableAdapter` and
`LambdaHandlerAdapter`:
```swift
let lambdaHandler = MyHandler()
let handler = LambdaCodableAdapter(
encoder: JSONEncoder(),
decoder: JSONDecoder(),
handler: LambdaHandlerAdapter(handler: lambdaHandler)
)
let runtime = LambdaRuntime(handler: handler)
```
2. Use a closure-based initializer that indirectly calls the handler:
```swift
let lambdaHandler = MyHandler()
let runtime = LambdaRuntime { event, context in
try await lambdaHandler.handle(event, context: context)
}
```
Both approaches are verbose and don't provide a clean, ergonomic API for
the common case of initializing `LambdaRuntime` with a custom
`LambdaHandler` instance. The closure approach also creates an
unnecessary indirection layer, wrapping the handler in a
`ClosureHandler` before adapting it, or using `LambdaCodableAdapter`.
### Modifications:
Added a new convenience initializer to `LambdaRuntime` in
`Lambda+JSON.swift` that accepts a `LambdaHandler` instance directly:
```swift
public convenience init<Event: Decodable, Output, LHandler: LambdaHandler>(
decoder: JSONDecoder = JSONDecoder(),
encoder: JSONEncoder = JSONEncoder(),
logger: Logger = Logger(label: "LambdaRuntime"),
lambdaHandler: sending LHandler
)
where
Handler == LambdaCodableAdapter<
LambdaHandlerAdapter<Event, Output, LHandler>,
Event,
Output,
LambdaJSONEventDecoder,
LambdaJSONOutputEncoder<Output>
>,
LHandler.Event == Event,
LHandler.Output == Output
```
This initializer handles the wrapping of the `LambdaHandler` with the
necessary adapters internally, matching the pattern already established
for closure-based handlers.
### Result:
Developers can now initialize `LambdaRuntime` with a `LambdaHandler`
instance using a clean, direct API:
```swift
let lambdaHandler = MyHandler()
let runtime = LambdaRuntime(lambdaHandler: lambdaHandler)
```
This provides:
- **Better ergonomics**: More intuitive and less verbose API
- **Consistency**: Matches the pattern of accepting handlers directly,
similar to how `StreamingLambdaHandler` can be used
- **No extra indirection**: Avoids wrapping the handler in an
unnecessary `ClosureHandler` or `LambdaCodableAdapter`
- **Type safety**: Maintains full type inference for `Event` and
`Output` types from the handler
Apply recommendations in code and documentation
- [CI] restrict permissions to read-all instead of the default write-all
- All examples README.md : add a note about Lambda functions
configuration with improved security and scalability changes for
production environment
- Swift docc documentation: add a note about Lambda functions
configuration with improved security and scalability changes for
production environment
---------
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
All the examples using SAM have a default Lambda runtime environment
memory size of 512Mb.
Lambda functions run in a microVM defined by its memory size. The memory
size influences the CPU power.
(see
https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html)
Increasing memory size increases runtime performance but also increase
costs.
As most of our examples are very simple and small functions, 512Mb
memory is not required. This PR reduces Lambda runtime execution
environment to 128Mb to reduce AWS costs.
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
In preparation for the 2.0.0 GA release,
- Update `.swift-version`, `Package.swift` and all examples'
`package.swift` to Swift 6.2
- Update all references to `2.0.0-beta.3` to `2.0.0`. This includes the
doc and readme, but also the dependencies in the examples
`Package.swift`. This will temporary break the build of the examples,
until we tag v2.0.0. Note the CI will not be affected as its consumes
the local version of the library
- [CI] Use Swift-6.2-noble for all testing tasks
- Reinstate the script to generate the contributors list and update the
list
Make required changed to ensure the library compiles when no traits are
enabled (fix:
https://github.com/swift-server/swift-aws-lambda-runtime/issues/562)
Add an example project that serves as test in the CI
### Motivation:
Recent changes introduced compilation errors when no traits are enabled.
### Modifications:
- `LambdaResponseStreamWriter.writeStatusAndHeaders()` moved to the
`FoundationSupport` directory where all classes and struct depending on
`Encodable` and `Decodable` are located, protected by `#if
FoundationJSONSupport`
- `LambdaRuntime.run()` method when ServiceLifeCycle is disabled in now
public (and therefore can not be `@inlinable` anymore)
- Add an example that disables all traits.
- Add this example to the CI
### Result:
The Library now compiles when no default traits are enabled.
This is flagged `semver/major` because we change the public API
`LambdaRuntime.run()`
Allows users to define on which port the Local server listens to, using
the `LOCAL_LAMBDA_PORT` environment variable.
While being at it, I also added `LOCAL_LAMBDA_HOST` if the user wants to
bind on a specific IP address.
I renamed `LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT` to
`LOCAL_LAMBDA_INVOCATION_ENDPOINT` for consistency.
### Motivation:
Addresses
https://github.com/swift-server/swift-aws-lambda-runtime/issues/556
### Modifications:
- When run outside of the Lambda execution environment, check for the
value of `LOCAL_LAMBDA_PORT` and passes it down to the Lambda HTTP Local
Server and runtime client.
- Add a unit test
### Result:
```
LAMBDA_USE_LOCAL_DEPS=../.. LOCAL_LAMBDA_PORT=8888 swift run
2025-09-01T21:55:22+0200 info LambdaRuntime: host="127.0.0.1" port=8888 [AWSLambdaRuntime] Server started and listening
```
- Use the new Swift 6 `@available` macro to remove requirement on
`.platform` in Package.swift.
- DRY: define the swift settings once for all in `Package.swift`
### Motivation:
- Remove the requirement to build on macOS 15 in `Package.swift`. This
allows library builders and end users to be more flexible on their
dependency requirements.
- The code is optionally compiled on macOS 15 and Linux, but SPM don't
enforce it anymore.
- Avoid repeating ourself. Be sure the same settings are applied on all
targets.
### Modifications:
- Create a `var swiftSetting: [SwiftSettings]` and reuse it for all
targets.
- Use `AvailabilityMacro=LambdaSwift 2.0:macOS 15.0`
- Add this on top of the majority struct / classes
```swift
#if swift(>=6.1)
@available(LambdaSwift 2.0, *)
#endif
```
### Result:
When using Swift 6.1, there is no more SPM dependency on macOS 15
Split the long `LambdaRuntimeClient.swift` file in two parts: the
`LambdaRuntimeClient` and the `LambdaChannelHandler` for easier reading
and maintenance
This PR implements a mechanism to propagate connection loss information
from the Lambda runtime client to the runtime loop, enabling termination
without backtrace when the connection to the Lambda control plane (or a
Mock Server) is lost.
The changes are:
- When the connection is lost,
`ChannelHandlerDelegate.channelInnactive()` now correctly calls
`resume(throwing:)` on the ending continuation, for all states
(`.waitingForNextInvocation ` and `.sentResponse`). This eliminates the
hangs on connection lost..
- I added top-level error handling on `LambdaRuntime._run()`
- Add a unit test to check that either
`LambdaruntimeError.connectionToControlPlaneLost`, a `ChannelError`, or
an `IOError` is thrown when the server closes the connection
Instant's value now correctly prints as an EPOCH number
### Motivation:
A regression was introduced by
https://github.com/swift-server/swift-aws-lambda-runtime/pull/540. The
HTTP headers returned by `LocalServer` contained an invalid
representation of the Lamba Deadline.
See https://github.com/swift-server/swift-aws-lambda-runtime/issues/551
### Modifications:
- Add `CustomStringConvertible` to `LambdaClock.Instant` to just print
the `Int64` value
- add a unit test
### Result:
The runtime works correctly with the new `LambdaClock`
Revert streaming codable handler change and propose it as an example
instead of an handler API.
**Motivation:**
I made a mistake when submitting this PR
https://github.com/swift-server/swift-aws-lambda-runtime/pull/532
It provides a Streaming+Codable handler that conveniently allows
developers to write handlers with `Codable` events for streaming
functions.
This is a mistake for three reasons:
- This is the only handler that assumes a Lamba Event structure as
input. I added a minimal `FunctionUrlRequest` and `FunctionURLResponse`
to avoid importing the AWS Lambda Events library. It is the first
handler to be event-specific. I don't think the runtime should introduce
event specific code.
- The handler only works when Lambda functions are exposed through
Function URLs. Streaming functions can also be invoke by API or CLI.
- The handler hides `FunctionURLRequest` details (HTTP headers, query
parameters, etc.) from developers
Developers were unaware they were trading flexibility for convenience
The lack of clear documentation about these limitations led to incorrect
usage patterns and frustrated developers who needed full request control
or were using other invocation methods.
**Modifications:**
- Removed the Streaming+Codable API from the library
- Moved the Streaming+Codable code to an example
- Added prominent warning section in the example README explaining the
limitations
- Clarified when to use Streaming+Codable vs ByteBuffer approaches
- Added decision rule framework to help developers choose the right
approach
**Result:**
The only API provided by the library to use Streaming Lambda functions
is exposing the raw `ByteBuffer` as input, there is no more `Codable`
handler for Streaming functions available in the API. I kept the
`Streaming+Codable` code an example.
After this change, developers have clear guidance on when to use each
streaming approach:
- Use streaming codable for Function URL + JSON payload + no request
details needed
- Use ByteBuffer StreamingLambdaHandler for full control, other
invocation methods, or request metadata access
This prevents misuse of the API and sets proper expectations about the
handler's capabilities and limitations, leading to better developer
experience and fewer integration issues.
Fix
[#384](https://github.com/swift-server/swift-aws-lambda-runtime/issues/384)
Note: this PR introduces an API change that will break Lambda functions
using `LambdaContext`, we should integrate this change during the beta
otherwise it will require a major version bump.
### Motivation:
`DispatchWallTime` has no public API to extract the time in
milliseconds, making it a dead end.
Previous implementation used the internal representation of time inside
`DispatchWallTime` to extract the value, creating a risk if its
implementation will change in the future.
Moreover, the use of `DispatchWallTime` obliges users to import the
`Dispatch` library or `Foundation`.
Old Code:
```
extension DispatchWallTime {
@usableFromInline
init(millisSinceEpoch: Int64) {
let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000
let seconds = UInt64(nanoSinceEpoch / 1_000_000_000)
let nanoseconds = nanoSinceEpoch - (seconds * 1_000_000_000)
self.init(timespec: timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds)))
}
var millisSinceEpoch: Int64 {
Int64(bitPattern: self.rawValue) / -1_000_000
}
}
```
Issue
[#384](https://github.com/swift-server/swift-aws-lambda-runtime/issues/384)
has a long discussion about possible replacements, including creating a
brand new `UTCClock`, which I think is an overkill for this project.
Instead, I propose this simple implementation, based on two assumptions:
- AWS always sends the time in milliseconds since Unix Epoch (1st Jan
1970) ([Lambda Runtime API
documentation](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next))
- AWS always uses UTC time (not only for Lambda, this is a general rule
for all AWS APIs) ([TZ=UTC on
Lambda](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html))
Therefore, this library just needs to store and make math on
milliseconds since epoch, without having to care about timezone.
I had two possibilities to implement the storage and the math on
milliseconds since Unix Epoch: either I could use an `UInt64` (as does
[the Rust
implementation](https://github.com/awslabs/aws-lambda-rust-runtime/blob/aff8d883c62997ef2615714dce9f7ddfd557147d/lambda-runtime/src/types.rs#L70))
or I could use a standard Swift type, such as `Duration`.
`Duration` is a good candidate for this because 1/ the time we receive
from the Lambda Service API is indeed a duration between 1/1/1970 and
the execution deadline for the Lambda function, expressed in
milliseconds, 2/ it gives a strong type that can be verified by the
compiler, and 3/ it is possible to do basic arithmetic operations and
compare two values.
As an additional benefit, it allows library users to not import
`Dispatch` or `Foundation`
### Modifications:
I made two changes:
1. I extend the `Duration` type to provide us with simple unix epoch
time manipulation functions and values.
```swift
extension Duration {
/// Returns the time in milliseconds since the Unix epoch.
@usableFromInline
static var millisSinceEpoch: Duration {
var ts = timespec()
clock_gettime(CLOCK_REALTIME, &ts)
return .milliseconds(Int64(ts.tv_sec) * 1000 + Int64(ts.tv_nsec) / 1_000_000)
}
/// Returns a Duration between Unix epoch and the distant future
@usableFromInline
static var distantFuture: Duration {
// Use a very large value to represent the distant future
millisSinceEpoch + Duration.seconds(.greatestFiniteMagnitude)
}
/// Returns the Duration in milliseconds
@usableFromInline
func milliseconds() -> Int64 {
Int64(self / .milliseconds(1))
}
/// Create a Duration from milliseconds since Unix Epoch
@usableFromInline
init(millisSinceEpoch: Int64) {
self = .milliseconds(millisSinceEpoch)
}
}
```
3. I replaced all references to `DispatchWallTime` by `Duration`
### Result:
No more `DispatchWallTime`
No dependencies on Foundation, as I use `clock_gettime()` to get the
epoch from the system clock.
re-implement MAX_INVOCATIONS and fix shell script
Fix https://github.com/swift-server/swift-aws-lambda-runtime/issues/377
### Motivation:
In v1, there was a script measuring the performance of the invocation
loop.
Re-instate this script to allow users and developers to measure the
performance impact of their changes.
### Modifications:
I re-implemented MAX_INVOCATIONS, to avoid the client looping against
the Mock Server. But this time, MAX_INVOCATIONS is handled on the
server, not on the client.
I slightly modified the script to work with v2 and the new MockServer.
### Result:
The script works.
This PR has a dependency on
https://github.com/swift-server/swift-aws-lambda-runtime/issues/465
Do not use a String for Lambdacontext.ClientContext, use a struct instead.
Fix for https://github.com/swift-server/swift-aws-lambda-runtime/issues/169
Note: this PR introduces an API change that will break function using
`LambdaContext`, we should integrate this change during the beta
otherwise it will require a major version bump.
### Motivation:
Let the compiler detect type errors for us
### Modifications:
- Create a struct for ClientContext and it's embedded ClientApplication
- add three unit test to validate the struct
### Result:
No more String?
---------
Co-authored-by: Konrad `ktoso` Malawski <konrad.malawski@project13.pl>
```
note: The majority of content should be under level-3 headers under the "Overview" section
--> Deployment.md:22:1-22:17
20 | * [Third-party tools](#third-party-tools)
21 |
22 + ## Prerequisites
| ╰─suggestion: Change the title to "Overview"
23 |
24 | 1. Your AWS Account
```
and many others.
Action: I lowered all titles one level down.
Add user-facing API for Streaming Lambda functions that receives JSON
events
### Motivation:
Streaming Lambda functions developed by developers had no choice but to
implement a handler that receives incoming data as a `ByteBuffer`. While
this is useful for low-level development, I assume most developers will
want to receive a JSON event to trigger their streaming Lambda function.
Going efficiently from a `ByteBuffer` to a Swift struct requires some
code implemented in the `JSON+ByteBuffer.swift` file of the librray. We
propose to further help developers by providing them with a new
`handler()` function that directly receives their `Decodable` type.
### Modifications:
This PR adds a public facing API (+ unit test + updated README) allowing
developers to write a handler method accepting any `Decodable` struct as
input.
```swift
import AWSLambdaRuntime
import NIOCore
// Define your input event structure
struct StreamingRequest: Decodable {
let count: Int
let message: String
let delayMs: Int?
}
// Use the new streaming handler with JSON decoding
let runtime = LambdaRuntime { (event: StreamingRequest, responseWriter, context: LambdaContext) in
context.logger.info("Received request to send \(event.count) messages")
// Stream the messages
for i in 1...event.count {
let response = "Message \(i)/\(event.count): \(event.message)\n"
try await responseWriter.write(ByteBuffer(string: response))
// Optional delay between messages
if let delay = event.delayMs, delay > 0 {
try await Task.sleep(for: .milliseconds(delay))
}
}
// Finish the stream
try await responseWriter.finish()
// Optional: Execute background work after response is sent
context.logger.info("Background work: processing completed")
}
try await runtime.run()
```
This interface provides:
- **Type-safe JSON input**: Automatic decoding of JSON events into Swift
structs
- **Streaming responses**: Full control over when and how to stream data
back to clients
- **Background work support**: Ability to execute code after the
response stream is finished
- **Familiar API**: Uses the same closure-based pattern as regular
Lambda handlers
Because streaming Lambda functions can be invoked either directly
through the API or through Lambda Function URL, this PR adds the
decoding logic to support both types, shielding developers from working
with Function URL requests and base64 encoding.
We understand these choice will have an impact on the raw performance
for event handling. Those advanced users that want to get the maximum
might use the existing `handler(_ event: ByteBuffer, writer:
LambaStreamingWriter)` function to implement their own custom decoding
logic.
This PR provides a balance between ease of use for 80% of the users vs
ultimate performance, without closing the door for the 20% who need it.
### Result:
Lambda function developers can now use arbitrary `Decodable` Swift
struct or Lambda events to trigger their streaming functions. 🎉
---------
Co-authored-by: Tim Condon <0xTim@users.noreply.github.com>
Add a log statement in the Lambda loop, before calling the user's
handler to show the raw payload before any attempt to decode it.
This was available in Runtime v1 and is now ported to v2.
This fixes
https://github.com/swift-server/swift-aws-lambda-runtime/issues/404
### Motivation:
This is useful when handling custom event and there is a Decoding error.
It allows to see the exact payload received by the handler before any
attempt to decode it.
### Modifications:
Add a log.trace statement with metatadata. Metadata are computed only
when the log level is trace or below.
### Result:
```
2025-07-21T08:58:33+0200 trace LambdaRuntime : Event's first bytes={"name": "me", "age": 50} aws-request-id=769127502334125 [AWSLambdaRuntime] sending invocation event to lambda handler
```
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Add a hard coded version number to the user agent string, for an
eventual identification by the Lambda service
### Motivation:
It's [an
issue](https://github.com/swift-server/swift-aws-lambda-runtime/issues/108)
that was open more than 5 years ago and was never addressed. At the
time, the consensus was to pickup a version number for the Package.swift
file and the maintainer at the time decided to wait for Swift to
implement this.
Five years later, and several major version of Swift later, this is
still not available. I decided to move on and implement a less optimal
solution. This can be replaced in the future if package version ever
becomes part of Package.swift.
### Modifications:
Add a version enum to isolate the versioning in one place. I decided to
keep it simple and not over engineering it with major, minor, patch and
pre-release. At the time, it's a simple string. This is all what we need
for usage in the user agent string.
### Result:
User agent now identifies as `Swift-Lambda/2,0` instead of
`Swift-Lambda/unknown`
This is a proposal to fix issue #507
**changes**
- `LambdaRuntime.init()` uses a `Mutex<Bool>` to make sure only one
instance is created
- `LambdaRuntime.init()` can now throw an error in case an instance
already exists (I did not use `fatalError()` to make it easier to test)
- All `convenience init()` methods catch possible errors instead of
re-throwing it to a void breaking the user-facing API
- Renamed existing `LambdaRuntimeError` to `LambdaRuntimeClientError`
- Introduced a new type `LambdaRuntimeError` to represent the double
initialization error
---------
Co-authored-by: Fabian Fett <fabianfett@apple.com>
Co-authored-by: Adam Fowler <adamfowler71@gmail.com>
Allow user to give their logger to the LambdaRuntime.
### Motivation:
Overloaded versions of `LambdaRuntime.init` don't allow to pass a logger
### Modifications:
Add a `logger` parameter to overloaded versions of `LambdaRuntime.init`
### Result:
It is now possible to write
```
let runtime = LambdaRuntime(logger: Logger(label: "MyLogger"), body: handler)
```
- Add ServiceLifecycle version of `LambdaRuntime.run` that wraps
internal `_run` call in `cancelOnGracefulShutdown`
- Add cancellation handlers for shutting down existing connections in
Local lambda
- Added test for lambda graceful shutdown
### Motivation:
Ensure local lambda supports graceful shutdown