Skip to content

Commit 0352f9a

Browse files
committed
feat: support Lambda SnapStart
Bridge the Lambda SnapStart lifecycle to the inner web application. Because the adapter runs the app as a separate process, the app has no access to the snapshot boundary; two optional HTTP hooks give it one. - AWS_LWA_SNAPSTART_BEFORE_CHECKPOINT_PATH: POSTed before the snapshot so the app can drain resources that will not survive it. - AWS_LWA_SNAPSTART_AFTER_RESTORE_PATH: POSTed after restore so the app can reconnect, refresh credentials, reseed randomness and regenerate per-environment identifiers. Both are opt-in and independent. A non-2xx response or a connection failure fails the corresponding SnapStart phase rather than serving traffic against an improperly prepared application. Each hook waits for the readiness check first, so AWS_LWA_ASYNC_INIT cannot let a hook fire before the app is listening. After restore the adapter also rebuilds its own HTTP client, so no connection captured in the snapshot is reused, and re-runs the readiness check before traffic is admitted. The hook paths are control-plane routes, so external requests that resolve to one receive 403. The guard canonicalizes both sides through the same Url::set_path transformation and compares segment lists, so alternate spellings -- percent encoding, encoded slashes, matrix parameters, dot segments, case, repeated slashes -- are blocked too. A hook path the guard cannot cover exactly (not canonicalizable, decoding to a literal %, collapsing to the app root, or colliding with AWS_LWA_PASS_THROUGH_PATH) fails initialization instead of running with a partially protected route. Connection pooling is disabled for the pre-snapshot client because CLOCK_MONOTONIC does not advance across the snapshot gap, which makes hyper's idle accounting unreliable for entries pooled before it (hyper#3810, rust-lang/rust#79462). The client rebuilt after restore pools normally, since it holds no pre-boundary entries -- that is what makes the new AWS_LWA_POOL_IDLE_TIMEOUT_SECONDS effective on the invocations that serve traffic. Also adds AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS to bound the readiness wait at cold start and after restore, bumps lambda_http to 1.3.0 for the SnapStart lifecycle APIs, fixes AWS_LWA_REMOVE_BASE_PATH to strip exactly one leading occurrence on a segment boundary, and ships deployable FastAPI examples for both zip and container packaging. Validated on a deployed SnapStart container function: both hooks fire, the 403 guard blocks the full equivalence class through API Gateway while distinct routes still 404, post-restore idle reuse works, the after-restore reseed yields distinct per-environment identifiers, and graceful-shutdown SIGTERM still arrives on restored environments.
1 parent 986113f commit 0352f9a

27 files changed

Lines changed: 3411 additions & 115 deletions

File tree

CHANGELOG.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,67 @@
1+
## Unreleased
2+
3+
### Features
4+
5+
- Add SnapStart support. The adapter notifies your web application at the SnapStart
6+
boundary via two opt-in HTTP hooks — `AWS_LWA_SNAPSTART_BEFORE_CHECKPOINT_PATH`
7+
(before checkpoint) and `AWS_LWA_SNAPSTART_AFTER_RESTORE_PATH` (after restore) —
8+
so it can drain and re-establish connections. Each hook call is bounded by a
9+
60-second timeout. After restore the adapter refreshes its own HTTP client and
10+
re-runs the readiness check before admitting traffic, and it rejects external
11+
traffic to the hook paths with 403.
12+
- The crate now stops publishing to crates.io (`publish = false`): Lambda Web
13+
Adapter ships as the `lambda-adapter` binary (a Lambda layer / copied
14+
extension), not as a library, so the `lib` target has no external
15+
API-stability contract. The internal changes SnapStart required — the
16+
`tower::Service` impl's `Response` is now `Response<BoxBody<Bytes, Error>>`
17+
(was `Response<Incoming>`) and `check_init_health` now returns `Result`
18+
therefore do not affect any published API. `Bytes` and `BoxBody` are
19+
re-exported for convenience of in-repo `Service` users. Existing crates.io
20+
consumers keep the last published release (`1.0.0-rc1`) unchanged.
21+
- Add `AWS_LWA_POOL_IDLE_TIMEOUT_SECONDS` to configure the idle keep-alive
22+
(fractional seconds allowed, e.g. `0.5`) of the adapter's HTTP connection to your
23+
app. Default: 4 seconds. A value that is set but unusable falls back to the
24+
default and logs a warning.
25+
- Add `AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS` to bound the readiness check
26+
(fractional seconds allowed, e.g. `0.5`), applied to both the initial cold-start
27+
readiness wait and the
28+
post-SnapStart-restore readiness check. When set and the app does not become
29+
ready within it, the adapter **refuses to serve**: cold-start init fails (the
30+
runtime never starts) and a restore fails, rather than admitting traffic to an
31+
app that never reported ready. When unset (the default) the wait is
32+
**unbounded**, matching the previous behavior, so existing slow-cold-start apps
33+
are unaffected unless they opt in. The `async_init` initial-readiness path keeps
34+
its own fixed ~9.8s bound (non-fatal) and is not affected by this variable.
35+
36+
### Bug Fixes
37+
38+
- Fix `AWS_LWA_REMOVE_BASE_PATH` stripping to remove exactly one leading occurrence
39+
on a path-segment boundary. Previously it used `trim_start_matches`, which stripped
40+
the prefix repeatedly and byte-wise: with `AWS_LWA_REMOVE_BASE_PATH=/api`,
41+
`/api/api/order` became `/order` (both copies removed) and `/apiorder` became
42+
`/order` (a partial segment stripped). Now `/api/api/order``/api/order` and
43+
`/apiorder` is passed through unchanged, and a configured trailing slash (`/api/`)
44+
is normalized so it behaves like `/api`. **Upgrade note:** this changes the path
45+
forwarded to your app for those inputs — deployments that relied on the old
46+
repeated/partial stripping should verify their routes.
47+
- Fix the before-checkpoint hook firing before the application is ready. With
48+
`AWS_LWA_ASYNC_INIT=true` the adapter finishes initialization after 9.8 seconds
49+
even if the app has not bound its port yet; the hook `POST` then failed
50+
immediately with a connection error and failed the SnapStart initialization phase.
51+
Both hooks now wait for the readiness check first, bounded by
52+
`AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS` when it is set.
53+
54+
### Dependencies
55+
56+
- Bump `lambda_http` to 1.3.0 (from 1.1.1) for the SnapStart lifecycle APIs.
57+
**Note:** this changes the `Cookie` header the inner application receives on
58+
every deployment, SnapStart or not — a multi-entry API Gateway v2 `cookies` array
59+
is now joined with `"; "` instead of `";"`, so your app sees `a=1; b=2` rather
60+
than `a=1;b=2`. That is the RFC 6265 form and frameworks accept both, but code
61+
that splits on a bare `;` without trimming will see leading spaces.
62+
63+
---
64+
165
## v1.0.1 - 2026-05-28
266

367
### Bug Fixes

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@ keywords = ["AWS", "Lambda", "APIGateway", "ALB", "API"]
1111
license = "Apache-2.0"
1212
homepage = "https://github.com/aws/aws-lambda-web-adapter"
1313
repository = "https://github.com/aws/aws-lambda-web-adapter"
14-
documentation = "https://docs.rs/lambda_web_adapter"
1514
categories = ["web-programming::http-server"]
1615
readme = "README.md"
1716
exclude = ["examples"]
17+
# Lambda Web Adapter ships as the `lambda-adapter` binary (packaged as a Lambda
18+
# layer / container-copied extension), not as a library. This stops publishing the
19+
# crate to crates.io: the `lib` target is an internal implementation detail of the
20+
# binary and tests, with no external API-stability contract.
21+
#
22+
# Note for crates.io consumers: earlier `0.x` / `1.0.0-rc1` releases remain
23+
# available and unchanged; they are simply the last published versions. New
24+
# development ships only as the binary/layer.
25+
publish = false
1826

1927
[dependencies]
2028
bytes = "1.9.0"
@@ -23,7 +31,7 @@ http-body = "1.0.1"
2331
http-body-util = "0.1.0"
2432
hyper = { version = "1.5.2", features = ["client"] }
2533
hyper-util = "0.1.10"
26-
lambda_http = { version = "1.1.1", default-features = false, features = [
34+
lambda_http = { version = "1.3.0", default-features = false, features = [
2735
"apigw_http",
2836
"apigw_rest",
2937
"alb",

README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The same docker image can run on AWS Lambda, Amazon EC2, AWS Fargate, and local
1515
- Supports Amazon API Gateway Rest API and Http API endpoints, Lambda Function URLs, and Application Load Balancer
1616
- Supports Lambda managed runtimes, custom runtimes and docker OCI images
1717
- Supports Lambda Managed Instances for multi-concurrent request handling
18+
- Supports Lambda SnapStart with before-checkpoint and after-restore hooks
1819
- Supports any web frameworks and languages, no new code dependency to include
1920
- Automatic encode binary response
2021
- Enables graceful shutdown
@@ -59,13 +60,17 @@ The readiness check port/path and traffic port can be configured using environme
5960
| AWS_LWA_READINESS_CHECK_PROTOCOL | readiness check protocol: "http" or "tcp" | "http" |
6061
| AWS_LWA_READINESS_CHECK_HEALTHY_STATUS | HTTP status codes considered healthy (e.g., "200-399") | "100-499" |
6162
| AWS_LWA_ASYNC_INIT | enable asynchronous initialization for long initialization functions | "false" |
62-
| AWS_LWA_REMOVE_BASE_PATH | the base path to be removed from request path | None |
63+
| AWS_LWA_REMOVE_BASE_PATH | base path to remove from the request path; strips exactly one leading occurrence on a segment boundary (with `/api`: `/api/api/order`->`/api/order`, `/apiorder` unchanged; trailing slash normalized) | None |
6364
| AWS_LWA_ENABLE_COMPRESSION | enable gzip/br compression for response body (buffered mode only) | "false" |
6465
| AWS_LWA_INVOKE_MODE | Lambda function invoke mode: "buffered" or "response_stream" | "buffered" |
6566
| AWS_LWA_PASS_THROUGH_PATH | the path for receiving event payloads from non-http triggers | "/events" |
6667
| AWS_LWA_AUTHORIZATION_SOURCE | a header name to be replaced to `Authorization` | None |
6768
| AWS_LWA_ERROR_STATUS_CODES | HTTP status codes that will cause Lambda invocations to fail (e.g. "500,502-504") | None |
6869
| AWS_LWA_LAMBDA_RUNTIME_API_PROXY | overwrites `AWS_LAMBDA_RUNTIME_API` to allow proxying request | None |
70+
| AWS_LWA_SNAPSTART_BEFORE_CHECKPOINT_PATH | inner-app path the adapter POSTs to before a SnapStart snapshot (drain resources) | None |
71+
| AWS_LWA_SNAPSTART_AFTER_RESTORE_PATH | inner-app path the adapter POSTs to after a SnapStart restore (reconnect/reseed) | None |
72+
| AWS_LWA_POOL_IDLE_TIMEOUT_SECONDS | idle keep-alive (seconds) for the adapter's connection to your app | "4" |
73+
| AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS | seconds (fractional allowed, e.g. 0.5) to wait for the app to report ready (cold-start init and after a SnapStart restore); on expiry the adapter FAILS (init fails and the runtime never starts; a restore fails) rather than serving. Unset, 0, or negative all mean wait indefinitely (a set-but-<=0 or malformed value is ignored with a warning). async_init keeps its own ~9.8s bound | unset / <=0 (unbounded) |
6974

7075
> **Deprecation Notice:** The following non-namespaced environment variables are deprecated and will be removed in version 2.0:
7176
> `HOST`, `READINESS_CHECK_PORT`, `READINESS_CHECK_PATH`, `READINESS_CHECK_PROTOCOL`, `REMOVE_BASE_PATH`, `ASYNC_INIT`.
@@ -75,6 +80,46 @@ The readiness check port/path and traffic port can be configured using environme
7580
7681
👉 [Detailed configuration docs](https://aws.github.io/aws-lambda-web-adapter/configuration/environment-variables.html)
7782

83+
### SnapStart support
84+
85+
When your function uses [Lambda SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html),
86+
the adapter can notify your web application at the snapshot boundary so it can
87+
drain and re-establish state (database connections, cached DNS, PRNG seeds,
88+
unique identifiers). Both hooks are opt-in and independent.
89+
90+
| Variable | When the adapter calls it | Use it to |
91+
|---|---|---|
92+
| `AWS_LWA_SNAPSTART_BEFORE_CHECKPOINT_PATH` | Before the snapshot is taken | Drain/close resources that won't survive the snapshot |
93+
| `AWS_LWA_SNAPSTART_AFTER_RESTORE_PATH` | After restore, before serving traffic | Reconnect, refresh credentials, reseed randomness, regenerate unique IDs |
94+
95+
Each hook is an empty `POST`; your application must respond with a `2xx` status.
96+
A non-`2xx` response or a connection failure fails the SnapStart phase
97+
(initialization for the before-checkpoint hook, restore for the after-restore hook)
98+
instead of serving traffic against an improperly prepared application. The adapter
99+
does not impose its own deadline on a hook — Lambda bounds both phases, and the
100+
after-restore hook in particular must finish within your function timeout.
101+
102+
After restore, the adapter also automatically refreshes its own HTTP connection
103+
to your application, so it never reuses a connection captured in the snapshot, and
104+
then re-runs the readiness check before admitting traffic. By default this wait is
105+
unbounded; set `AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS` (fractional seconds allowed)
106+
to bound it, in which case a restore whose application does not report ready within
107+
that timeout fails.
108+
109+
> These hook paths are control-plane operations. External requests (via API
110+
> Gateway or ALB) that target a configured hook path receive `403 Forbidden` and
111+
> are never forwarded to your application, so choose paths your normal traffic
112+
> does not use.
113+
>
114+
> **Warning:** that guard exists only while the adapter is in the request path —
115+
> that is, when your application runs on Lambda behind the adapter. The hook routes
116+
> are ordinary application routes that mutate state, so if you run the same image
117+
> or application **without** the adapter (Amazon ECS, Amazon EKS, a local Docker
118+
> host), they are reachable and unauthenticated. Don't expose them publicly in
119+
> those deployments, or protect them yourself.
120+
121+
See the [FastAPI with SnapStart example](examples/fastapi-snapstart-zip) for a complete, deployable application.
122+
78123
## Examples
79124

80125
- [FastAPI](examples/fastapi)
@@ -84,6 +129,8 @@ The readiness check port/path and traffic port can be configured using environme
84129
- [FastAPI with Response Streaming in Zip](examples/fastapi-response-streaming-zip)
85130
- [FastAPI with Response Streaming on Lambda Managed Instances](examples/fastapi-response-streaming-lmi)
86131
- [FastAPI Response Streaming Backend with IAM Auth](examples/fastapi-backend-only-response-streaming/)
132+
- [FastAPI with SnapStart](examples/fastapi-snapstart)
133+
- [FastAPI with SnapStart in Zip](examples/fastapi-snapstart-zip)
87134
- [Flask](examples/flask)
88135
- [Flask in Zip](examples/flask-zip)
89136
- [Serverless Django](https://github.com/aws-hebrew-book/serverless-django) by [@efi-mk](https://github.com/efi-mk)

docs/guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- [Non-HTTP Event Triggers](./features/non-http-events.md)
2424
- [Multi-Tenancy](./features/multi-tenancy.md)
2525
- [Lambda Managed Instances](./features/managed-instances.md)
26+
- [SnapStart](./features/snapstart.md)
2627
- [Graceful Shutdown](./features/graceful-shutdown.md)
2728
- [Base Path Removal](./features/base-path-removal.md)
2829
- [Authorization Header](./features/authorization-header.md)

docs/guide/src/configuration/environment-variables.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ All configuration is done through environment variables, set either in your Dock
1212
| `AWS_LWA_READINESS_CHECK_PROTOCOL` | Readiness check protocol: `http` or `tcp` | `http` |
1313
| `AWS_LWA_READINESS_CHECK_HEALTHY_STATUS` | HTTP status codes considered healthy (e.g. `200-399` or `200,201,204,301-399`) | `100-499` |
1414
| `AWS_LWA_ASYNC_INIT` | Enable asynchronous initialization | `false` |
15-
| `AWS_LWA_REMOVE_BASE_PATH` | Base path to remove from request path | None |
15+
| `AWS_LWA_REMOVE_BASE_PATH` | Base path to remove from the request path. Strips **exactly one** leading occurrence and only on a path-segment boundary: with `/api`, `/api/api/order``/api/order` and `/apiorder` is passed through unchanged; a configured trailing slash (`/api/`) is normalized. | None |
1616
| `AWS_LWA_ENABLE_COMPRESSION` | Enable gzip/br compression (buffered mode only) | `false` |
1717
| `AWS_LWA_INVOKE_MODE` | Invoke mode: `buffered` or `response_stream` | `buffered` |
1818
| `AWS_LWA_PASS_THROUGH_PATH` | Path for non-HTTP event payloads | `/events` |
1919
| `AWS_LWA_AUTHORIZATION_SOURCE` | Header name to replace with `Authorization` | None |
2020
| `AWS_LWA_ERROR_STATUS_CODES` | HTTP status codes that cause Lambda invocation failure (e.g. `500,502-504`) | None |
2121
| `AWS_LWA_LAMBDA_RUNTIME_API_PROXY` | Proxy URL for Lambda Runtime API requests | None |
22+
| `AWS_LWA_SNAPSTART_BEFORE_CHECKPOINT_PATH` | Inner-app path the adapter POSTs to before a SnapStart snapshot | None |
23+
| `AWS_LWA_SNAPSTART_AFTER_RESTORE_PATH` | Inner-app path the adapter POSTs to after a SnapStart restore | None |
24+
| `AWS_LWA_POOL_IDLE_TIMEOUT_SECONDS` | Idle keep-alive (seconds) for the adapter's connection to your app | `4` |
25+
| `AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS` | Seconds (fractional allowed, e.g. `0.5`) the adapter waits for the app to report ready (cold-start init **and** after a SnapStart restore). On expiry the adapter **fails** rather than serving: cold-start init fails (the runtime never starts) and a restore fails. Unset, `0`, or a negative value all mean **wait indefinitely** (no bound); a set-but-`<= 0` or malformed value is ignored with a `warn!`. The `async_init` path keeps its own ~9.8s bound (non-fatal) and is unaffected. | unset / `<= 0` (unbounded) |
2226

2327
## Deprecated Variables
2428

docs/guide/src/examples/overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The repository includes working examples for many popular web frameworks, packag
88
|---------|-----------|-----------|
99
| [FastAPI](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/fastapi) | Docker | No |
1010
| [FastAPI in Zip](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/fastapi-zip) | Zip | No |
11+
| [FastAPI SnapStart](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/fastapi-snapstart) | Docker | No |
12+
| [FastAPI SnapStart Zip](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/fastapi-snapstart-zip) | Zip | No |
1113
| [FastAPI Background Tasks](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/fastapi-background-tasks) | Docker | No |
1214
| [FastAPI Response Streaming](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming) | Docker | Yes |
1315
| [FastAPI Response Streaming Zip](https://github.com/aws/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming-zip) | Zip | Yes |

0 commit comments

Comments
 (0)