DEV Community

Cover image for SnapStart for Python Lambda

SnapStart for Python Lambda

Recently we built an analytical API on Lambda using Python 3.12, FastAPI and DuckDB to query Parquet files in S3. The functionality worked fine, but we ran into cold start issues, mainly from initializing DuckDB and FastAPI at startup. This article explains how we used Lambda SnapStart to reduce that cold start, and what we learned from the experience.

Why DuckDB?

DuckDB is an open-source, in-process analytical (OLAP) database engine. The simplest mental model is: it is SQLite, but optimized for analytics.

Because it runs in-process inside the Lambda, there is no database server or cluster to manage, and no database credentials to handle, as S3 is read through the Lambda execution role. It also queries Parquet directly, including Parquet objects in S3:

SELECT
    count(*) AS request_count,
    median(response_time_ms) AS median_ms,
    quantile_cont(response_time_ms, 0.95) AS p95_ms
FROM read_parquet(
    's3://example-bucket/website-requests/*.parquet'
);
Enter fullscreen mode Exit fullscreen mode

What is Lambda SnapStart?

Every time Lambda spins up a new environment, it runs your initialization code again. SnapStart changes when that happens: the init runs when you publish a version. Lambda takes a snapshot of the environment after it is initialized, encrypts it, and caches it. From then on, Lambda restores new environments from that snapshot instead of starting from scratch.

Here is what that looks like on a cold request:

Without SnapStart
  start the runtime
  -> run the initialization code
  -> handle the request

With SnapStart
  restore the snapshot
  -> handle the request
Enter fullscreen mode Exit fullscreen mode

SnapStart is currently available on three runtimes, but it is not free everywhere:

Runtime Extra SnapStart charge
Java 11 and later None
Python 3.12 and later Snapshot caching and restoration
.NET 8 and later Snapshot caching and restoration

Sample project

Let's try to explain SnapStart with a website-performance API. This API queries the performance of a website, with the data stored in S3. Built with FastAPI, it takes a date range and an optional page, then uses DuckDB to scan the Parquet files for request count, median and p95 response time, error count and error rate, overall and per page.

The complete source is available in the python_lambda_with_snapshot_support repository.

The CDK project in the repository deploys the following resources:

Architecture of the two-function SnapStart comparison

Implementation

The CDK stack deploys two Lambda functions:

  • web-perf-without-snapstart runs app.lambda_handler
  • web-perf-with-snapstart runs app_snapstart.lambda_handler and has SnapStart enabled

Everything else is identical: Python 3.12, x86_64, 1024 MB, a 30 second timeout, and the same ZIP.

The shared code sits in src/app.py, which holds the DuckDB query class, the FastAPI app and the Mangum handler. src/app_snapstart.py is a thin wrapper that imports it, registers the after-restore hook and re-exports the handler.

The sections below cover the code changes SnapStart needed.

Keep expensive initialization at module level

The DuckDB connection, the FastAPI app and the route registration all happen at module level in src/app.py, so web-perf-with-snapstart captures them in its snapshot.

analytics = DuckDBWebsiteAnalytics(
    dataset_uri=os.getenv(
        "WEBSITE_DATASET_URI",
        "data/website-requests/*.parquet",
    ),
    aws_region=os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION"),
)
app = FastAPI(
    title="Website performance analytics API",
    version="2.0.0",
    docs_url=None,
    redoc_url=None,
)
Enter fullscreen mode Exit fullscreen mode

Refreshing S3 Connection

DuckDB creates its S3 secret during init, so whatever credentials it resolved are part of the snapshot. One snapshot can then be restored many times over several hours.

So src/app_snapstart.py recreates that secret after every restore:

"""SnapStart entry point."""

from snapshot_restore_py import register_after_restore

import app


@register_after_restore
def refresh_credentials_after_restore() -> None:
    app.analytics.refresh_s3_credentials()


lambda_handler = app.lambda_handler
Enter fullscreen mode Exit fullscreen mode

Most functions don't need a hook. When SnapStart is on, the runtime switches to container credentials rather than the access-key environment variables, specifically so they don't expire before a restore, and SDK connections usually resume on their own. We rebuild the secret anyway: it's one cheap call, and AWS advises refreshing ephemeral data like temporary credentials in the handler even without SnapStart.

Keep hooks quick. Runtime load and all after-restore hooks share a 10-second budget before SnapStartTimeoutException.

Wiring it up in CDK

SnapStart is a function-level setting, and it applies to every version published from it:

with_snapstart = lambda_.Function(
    # identical configuration omitted
    snap_start=lambda_.SnapStartConf.ON_PUBLISHED_VERSIONS,
)

with_snapstart_alias = lambda_.Alias(
    self,
    "WithSnapStartLiveAlias",
    alias_name="live",
    version=with_snapstart.current_version,
)
Enter fullscreen mode Exit fullscreen mode

Ensure that API Gateway points to the alias. Pointing it at the function's unqualified ARN invokes $LATEST, which cannot use SnapStart, so cold starts stay slow.

Performance

Both functions ran with the same configuration:

  • Memory: 1024 MB
  • Region: us-east-1
  • Architecture: x86_64
  • Package type: ZIP

Test strategy: we built a harness that forces cold starts by publishing a fresh version, then firing 30 concurrent requests at each API. That gives 30 cold starts per function per endpoint, confirmed by Init Duration in the REPORT lines of the function without SnapStart, and Restore Duration in the REPORT lines of the SnapStart function. A SnapStart function's REPORT has no Init Duration field at all, because it initialized at publish time; that value lands in a separate INIT_REPORT record.

Client-side through API Gateway:

Endpoint Metric Without SnapStart With SnapStart Improvement
/health median 5478.9 ms 1189.0 ms 78.3%
/health p95 6136.9 ms 1349.6 ms 78.0%
/analytics/website median 7589.8 ms 4057.7 ms 46.5%
/analytics/website p95 8381.3 ms 4217.9 ms 49.7%

Improvement is (without - with) / without * 100.

Approximate monthly cost

The following cost is calculated for a SnapStart-enabled Lambda running a single published version in us-east-1 at 1024 MB. Lambda bills per GB-second, so 1024 MB appears as 1 GB in the calculations.

Assumptions:

  • 100,000 requests a month
  • 500 ms average billed duration
  • the version is active all month
  • 1% of invocations restore a snapshot
Component Calculation Monthly cost
Requests 100,000 ÷ 1,000,000 × $0.20 $0.02
Duration 100,000 × 0.5 s × 1 GB × $0.0000166667 $0.83
SnapStart cache 2,592,000 s (30 days) × 1 GB × $0.0000015046 $3.90
SnapStart restores 1,000 × 1 GB × $0.0001397998 $0.14
Total $4.89

The cache is the largest item, and it is billed on memory and uptime rather than traffic. Restores scale with how often Lambda creates environments, which invocation count alone cannot tell you:

Restore rate Restores Restore cost Total
1% 1,000 $0.14 $4.89
10% 10,000 $1.40 $6.15
100% 100,000 $13.98 $18.73

Gotchas

  • Every published version keeps its own cached snapshot, billed for at least three hours and for as long as the version exists. Delete the ones you don't need.
  • Lambda re-runs your init code when it patches snapshots, and bills you for it, so "runs once at publish" is not quite true.
  • Enabling SnapStart isn't enough. If API Gateway hits the unqualified ARN it gets $LATEST and no SnapStart.
  • Don't trust a connection opened during init.
  • One snapshot seeds many environments, so anything unique from init gets duplicated. Generate per-request values in the request.
  • Publishing is slower. The version sits in Pending until the snapshot is built, so wait for Active before moving the alias.
  • SnapStart can't be combined with provisioned concurrency. They solve the same problem in different ways, so pick one.
  • No Amazon EFS, no Amazon S3 Files, and no ephemeral storage above 512 MB. The last one matters for DuckDB, since large joins and sorts spill to /tmp.

Conclusion

SnapStart brought our initialization down from seconds to under a second, which is a real gain on a cold request. It does nothing for query time, and the snapshot cache costs money whether the function is called or not.

Worth trying, but measure it on your own workload before you commit to it.

References

Top comments (0)