動かして学ぶ AWS Lambda SnapStart の仕組み

~ コールドスタートの起動パフォーマンス向上のための技術解説 ~

2025-03-03
デベロッパーのためのクラウド活用方法

Author : 渡辺 紘久

AWS テクニカルアカウントマネージャーの渡辺です。

みなさまの中に AWS Lambda の起動パフォーマンスの問題に悩まれている方はいらっしゃいますでしょうか。もしいらっしゃればぜひ最後まで読んでいただけたらと思います。また、現時点では悩んでいない方も、今後遭遇するかもしれない Lambda の起動パフォーマンスの最適化を行う上での選択肢を増やすために、ぜひ読んでいただけたらと思います。

Lambda の起動パフォーマンスを向上させる仕組みとして、2022 年に Java 関数向け AWS Lambda SnapStart の発表 がされました。また、2024 年には対応言語が追加され、Python 関数と .NET 関数用の SnapStart のサポートを開始 しました。
現在では SnapStartは、Java, Python, .NET に 対応しています

SnapStart は通常、関数コードを変更することなく、わずか 1 秒未満の起動パフォーマンスを提供できます。SnapStart を使用すると、リソースをプロビジョニングしたり、複雑なパフォーマンス最適化を実装したりすることなく、応答性が高くスケーラブルなアプリケーションを簡単にビルドできます。

起動時のレイテンシーの最大の原因は、コールドスタートと呼ばれる Lambda が関数の初期化に費やす時間であり、SnapStart はこれを改善するための仕組みです。具体例としては、AWS re:Invent 2024 の SnapStart のセッション では、LangChain を使用した LLM アプリケーションでは 3.5 秒のコールドスタートが 500 ミリ秒に、DuckDB を使用したデータ分析では 8 秒が 1 秒に改善される例が紹介されました。

今回は、Python を用いて実際に手を動かしつつ、SnapStart について学んでいきたいと思います。

なお、本記事の内容は以下の動画でも解説しています。SnapStart の概要とデモをご覧いただけますので、手順や動作イメージの確認にご活用ください。デモの内容は同じですが、技術詳細については本記事でより詳しく解説しています。


SnapStart の仕組み

SnapStart は、Lambda 関数の初期化処理が終わった後に、実行環境のスナップショットを取って、コールドスタートに該当する起動の場合にスナップショットからのリストアを行うことでコールドスタートの場合の起動時間を短縮します。

コールドスタート

ここで、コールドスタートについてご説明します。

SnapStart を使わない場合の Lambda 関数の実行では、まず初期化フェーズとしてサーバーレスコンピューティングのためのセキュアな軽量のマイクロ仮想マシンである Firecracker の microVM の実行や、ランタイムの起動、関数コードのダウンロードや関数コードのうちハンドラー外の初期化コードの実行が行われます。続いて、ハンドラー内部のロジックが実行されます。

これがコールドスタートと呼ばれます。

ウォームスタート

Lambda は 数時間ごとに実行環境を終了 しますが、別の関数の呼び出しを想定して、実行環境をしばらく維持します。使用可能な実行環境が残っている場合、次回以降の起動ではハンドラー内部のロジックのみが実行されます。これがウォームスタートです。

Lambda は実行環境を再利用することはできますが、それは保証されていません。継続的に呼び出される関数であっても、実行環境の終了は発生 します。また、スケールアウトの際にも実行環境を新規作成 することがあります。

このような状況に対応するため、意図的なウォームスタートを起こす設定として 2019 年に Provisioned Concurrency が、コールドスタートの場合の起動時間を短縮する方法として 2022 年に Java 向けの SnapStart が登場しました。

SnapStart

続いて SnapStart の場合のご説明です。
SnapStart を使うためには、関数を起動する前にまず関数のバージョンの発行が必要です。

関数のバージョン発行のタイミングで、通常の起動の初期化フェーズに当たる処理が行われます。その後メモリとディスクの状態を暗号化した Firecracker microVM スナップショットが取得され、保存されて、高速なキャッシュに配置されます。

その後、ユーザーがバージョンを指定して実行すると、実行環境が作成されますが、このタイミングでスナップショットからのリストアが行われます。ハンドラー外の初期化コードが既に実行された状態でリストアされますのでその分の起動パフォーマンスが高速化されます。

その後の挙動は SnapStart 無しの場合の Lambda 関数と同じで、実行環境が残っていれば ウォームスタートとして呼び出しフェーズのみ実行されます。


ユースケース

以下のケースで恩恵を受けることができます。

  • Java や .NET など実行環境の準備に時間がかかる場合
  • Python において依存関係のロードやフレームワークの使用に時間がかかる場合

まずは Java のように実行環境の準備に時間がかかる場合に有効です。Java での事例として、builders.flash の Java × サーバーレスは SaaS バックエンドとして通用するのか ? ~ スタートアップの実戦記録 ~ という記事が参考になります。

今回追加された .NET は Java と同じようにランタイムのブートストラップやクラスのロードに時間がかかるので、ほとんどのユースケースで SnapStart の恩恵を受けられます。

  • AOT (Ahead of Time Compilation) に関する補足

    .NET 8 以降では、AOT (Ahead of Time Compilation) の恩恵を受けることができ、実行時にビルドを行う Just-in-time compilation が不要になりました。

    .NET の Ahead-of-Time (事前) コンパイルに関しては、最新バージョンでも特にトリミングの概念において課題があります。Ahead-of-Time コンパイルでは、アプリケーションが Just-in-Time コンパイル中に動的なルックアップを実行し、アプリケーションの特定の部分がトリミングされている場合、アプリケーションの動作は決定論的ではなくなります。このような場合、コードの変更なしでレイテンシーを削減するために、SnapStart と組み合わせた .NET の使用をお勧めします。これは、Java の AOT (Ahead of Time Compilation) である GraalVM についても同様です。


Python はインタープリター言語なので、言語ランタイムの起動は遅くないのですが、例えば生成 AI アプリを Lambda を利用して実現するような場合や、バッチ処理のような、LangChain や Numpy, Pandas など、依存するライブラリが多かったり、Flask や Django などのフレームワークを利用したり、あるいは初回にデータセットのダウンロードが走る場合など、 コールドスタートの影響が大きいケースに有効です。

なお、ランタイムごとのコールドスターにかかる時間についての参考情報として、AWS 非公式ではありますが こちら をご覧いただけます。


Python で SnapStart を試してみる

以下のコードを動かしてみます。(もし仕組みだけ知りたい方はここは飛ばしていただいても構いません)

まず、全体的にどの処理が動いたか分かるログ出力を入れています。

  • 8 行目と 9 行目あたりはハンドラー外の処理なので、コールドスタートの場合に実行されます。
  • 8 行目で現在の時刻をグローバル変数に入れて、9 行目で時間のかかる処理のシミュレートをするために 5 秒のスリープを入れています。
  • 13 行目のデコレーターで指定した関数がスナップショット作成直前に実行されます。
  • 18 行目のデコレーターで指定した関数がスナップショット復元後に実行されます。
  • 22 行目のハンドラー内の処理は今回はログ出力だけしています。
import time
from datetime import datetime, timezone, timedelta
from snapshot_restore_py import register_before_snapshot, register_after_restore

# グローバルスコープで時間の掛かる初期化処理をシミュレート
print("グローバルスコープの処理: 5秒掛かる初期化処理実行...")
# 変数に現在時刻を代入(JST)
initialized_time = (datetime.now(timezone(timedelta(hours=9))).isoformat(timespec='milliseconds').replace('+00:00', 'Z'))
time.sleep(5)  # 5 秒の時間の掛かる初期化処理を想定
print("グローバルスコープの処理: 初期化処理終了")

# スナップショット取得直前に実行される処理
@register_before_snapshot
def before_snapshot():
    print("スナップショット取得直前の処理実行...")

# スナップショット復元後に実行される処理
@register_after_restore
def after_restore():
    print("リストア後の処理開始...")

def lambda_handler(event, context):
    print("ハンドラー内の処理開始...")
    print(f"初期化した日時を格納した変数の値は、'{initialized_time}'")

    return {
        'statusCode': 200,
        'body': 'Function executed successfully'
    }

AWS マネジメントコンソールで Lambda コンソール を開きます。

画面右上の「関数を作成」を押下します。

クリックすると拡大します

関数名に任意の名前を入力します。ここでは SnapStart_Demo としています。

ランタイムに任意の Python バージョンを選択します。ここでは Python 3.13 としています。

それ以外はデフォルトのまま、画面右下の「関数の作成」を押下します。

クリックすると拡大します

デフォルトで入力されているコードを削除し、上述のデモ用コードを貼り付けます。

クリックすると拡大します

画面左下の「Deploy」を押下します。

クリックすると拡大します

画面上部に以下の表示がされれば Deploy 成功です。

クリックすると拡大します

この関数をテスト実行してみます。

テストタブに切り替えた後、右側の「テスト」ボタンを押下します。

この時、SnapStart 無しの初回実行となりますので、さきほどご説明させていただいた通り、初期化フェーズの後に呼び出しフェーズとなり、コールドスタートになります。そのため、テスト完了までに 5 秒程度かかります。

クリックすると拡大します

ログを見てみます。「ログ」を押下します。

クリックすると拡大します

Amazon CloudWatch のコンソールに遷移しました。作成されたログストリームを押下します。

クリックすると拡大します

ログが表示されます。ログから、グローバルスコープの処理とハンドラー内の処理が実行されており、コールドスタートであることが分かります。(なお任意ですが、画面右上の「表示」ボタンの左側で、ローカルタイムゾーンか UTC タイムゾーンを切り替えることが可能です)

クリックすると拡大します

再度 Lambda コンソールに戻ります。もう一度テスト実行するために、画面右側の「テスト」を押下します。

この時、初回の実行で作成された実行環境を再利用しますので、ウォームスタートとなります。見た目上、一瞬で完了します。(ここでもし再度 5 秒程度かかった場合は、何らかの要因で実行環境が再利用されずコールドスタートになっています。その場合はもう一度「テスト」を押下するとウォームスタートとなるはずです)

クリックすると拡大します

2 回目の実行のログを見てみます。ログを押下します。その後は先程同様にログストリームを押下してください。

クリックすると拡大します

2 回目の実行はハンドラー内の処理だけ行われており、ウォームスタートであることが分かります。(もしログが表示されていない場合、「アクション」の左側の更新ボタンを何度か押すことで表示されるはずです)

クリックすると拡大します

ここまでで、SnapStart 無しの場合のコールドスタートとウォームスタートを体験していただきました。かなりのレイテンシーの差を感じていただけたと思います。

ここからは、コールドスタートの場合の起動時間を短縮するために、SnapStart を有効にしていきます。

Lambda のコンソールに戻り、設定タブの「編集」を押下します。

クリックすると拡大します


SnapStart」 を「PublishedVersions」に変更して、右下の「保存」を押下します。

クリックすると拡大します

ここで、SnapStart を有効にするだけではスナップショットが取得されない点は注意が必要です。

スナップショットを取得するために、関数バージョンを発行します。

画面右上の「アクション」 - 「新しいバージョンを発行」を押下します。

クリックすると拡大します

今回はそのまま「発行」を押下します。

クリックすると拡大します

SnapStart によるバージョン作成処理が進行していることが分かります。

クリックすると拡大します

完了まで数分程度かかるため、ここでログを見てみます。

先ほど開いた CloudWatch のコンソールを開きます。(閉じた場合は、CloudWatch のコンソール - ロググループ - /aws/lambda/{Lambda 関数名} のロググループを開きます)

今回作成したバージョン1のログストリームが作成されていますので押下して開きます。(もしログストリームが作成されていない場合、「削除」の左側にある更新ボタンを押してみてください)

クリックすると拡大します

これはスナップショット取得時のログです。ログから、グローバルスコープの処理とスナップショット取得直前の処理が実行され、この状態のスナップショットが取得されたことが分かります。

クリックすると拡大します

図のように Lambda コンソールでバージョン 1 の作成が完了するまで待ちます。数分程度かかる場合があります。

クリックすると拡大します

バージョン 1 の作成が完了したら、再度テストを行います。

今回の実行では、SnapStart 有りのコールドスタートとなります。SnapStart が有効になっていますので、スナップショットから復元され、すぐに実行が完了します。

クリックすると拡大します

実行が完了したらログを見てみます。「ログ」を押下します。

クリックすると拡大します

最も新しいログストリームを開きます。

クリックすると拡大します

ログを見てみると、リストア後の処理の後にハンドラー内の処理が実行されています。スナップショットから復元されたことが分かります。初期化した日時を格納した変数には、関数実行時ではなく、スナップショット取得時の日時が格納されています。

クリックすると拡大します

再度バージョン 1 を実行してみます。Lambda コンソールに戻り、「テスト」を押下します。

今度はこの時、バージョン 1 の初回の実行で作成された実行環境を再利用しますので、ウォームスタートとなります。さきほどよりも早く完了します。

クリックすると拡大します

実行が完了したらログを見るために「ログ」を押下します。

クリックすると拡大します

最も新しいログストリームを開きます。

クリックすると拡大します

ログを見てみると、リストア後の処理は実行されておらず、ハンドラー内の処理のみ実行されています。変数の値はさきほどと同じ、スナップショット取得時の値となっています。ウォームスタートであることが分かります。(もしリストア後の処理が実行されているログである場合、アクションの左側の更新ボタンを押してみてください。それでもリストア後のログである場合、何らかの要因で実行環境が再利用されずコールドスタートになっています。その場合はもう一度「テスト」を押下すると ウォームスタートとなるはずです)

クリックすると拡大します

これで動作確認は以上です。コールドスタートとウォームスタートの違い、SnapStart 有りと無しの違いがご体感いただけたと思います。

SnapStart の仕組み」を再度読んでいただいたり、デモ用コードやログを見返したり、再度実行したりしてみると、より理解が深まると思います。


Infrastructure as Code (IaC) での記述方法

今回はマネジメントコンソールで SnapStart を設定しましたが、IaC の場合は以下のように記述します。

  • AWS CDK
from aws_cdk import (
    Stack,
    aws_lambda,
)
from constructs import Construct

class SnapStartStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        aws_lambda.Function(self, id="SnapStartFunction",
                          code=aws_lambda.Code.from_asset(path="src/"),
                          handler="MY_HANDLER",
                          runtime=aws_lambda.Runtime.PYTHON_3_13,
                          snap_start=aws_lambda.SnapStartConf.ON_PUBLISHED_VERSIONS)

Python の例をご紹介しましたが、TypeScript の例は以下をご参照ください。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.SnapStartConf.html

  • AWS SAM
SnapStartFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: src/
    Handler: MY_HANDLER
    Runtime: python3.13
    SnapStart:
      ApplyOn: PublishedVersions

Python の例をご紹介しましたが、その他の言語での例については以下をご参照ください。
https://github.com/aws-samples/enable-lambda-snapstart-with-iac-samples


スナップショット取得前後の処理

スナップショット作成前と復元後にカスタムコードを実行できます。例えばデモコードの before_snapshot 関数、after_restore 関数はそれぞれスナップショット取得直前と、スナップショットからの復元時に動く関数です。これらは不要であれば記述しなくても大丈夫ですが、以下のような場合にご活用いただけます。

  • スナップショット取得前: プログラミングにおいて、変数やオブジェクトのスコープをグローバルから限定的なものに小さく閉じ込めたい場合
    • ファイルのダウンロード
    • 計算負荷の高いタスク
  • スナップショット復元時
    • データベース接続処理
    • データリフレッシュ
    • シークレット情報の取得

Lambda ハンドラの外で HTTP 接続やデータベース接続を作成する場合、これらの接続は SnapStart がスナップショットを作成する時点で保存されます。

しかし、数時間後に Lambda 関数を呼び出すと、Lambda が別のサーバーで実行環境を復元する可能性があるため、あるいはダウンストリームの接続にタイムアウトなどの問題が発生している可能性があるため、その接続が利用できなくなっている場合があります。

SnapStart の復元時のカスタムコードを使用するか、 Lambda ハンドラ内で確認処理を実装することができます。

もう一つの考慮点は、シークレットに Amazon RDS の接続情報を保存して接続するようなケースなど、スナップショット作成時にシークレットや一時的な情報を取得する場合などの、一時的なデータに関するものです。ファイルやシークレット、その他の一時的なデータについて、復元後にそのデータが有効であると想定せずに検証する必要があります。例えば、5 日前にスナップショットを作成し、その間にローテーションされた可能性のある同じシークレットを使用している場合、問題が発生する可能性があります。

言語毎のコードの記述方法は ドキュメント をご参照ください。


考慮点

SnapStart は、基本的にはコードの変更をすることなく、SnapStart を有効にするだけで恩恵を受けることができますが、以下の場合に注意が必要です。

一意性 (Uniqueness)

一意性を扱う必要がある場合のシナリオでは、リストアは 1 つのスナップショットからされることに注意が必要です。SnapStart の使用時に一意性を維持するには、初期化後に一意のコンテンツを生成する必要があります。前述のスナップショット復元時のカスタムコードをご活用ください。

バージョンとエイリアス

前述の通り、SnapStart を有効にするだけではスナップショットからの復元がされないことに注意が必要です。バージョンを発行し、バージョンかエイリアスを指定して関数を実行する必要があります。

Provisioned Concurrency との比較

Lambda の起動パフォーマンスを向上するための仕組みとして、Provisioned Concurrency (関数に対するプロビジョニングされた同時実行数) があります。すでに多くのお客様が Provisioned Concurrency を使用しています。

Provisioned Concurrency についてご存じない方のために説明すると、設定した数の Lambda の実行環境を事前に初期化された実行環境としてウォームアップする機能です。例えば 100 個の実行環境を指定すると、それらは即座に応答できる状態で待機します。Provisioned Concurrency の理解には、こちらの builders.flash 記事 が役に立ちます。

Provisioned Concurrency は、設定した数の Lambda の実行環境に対してウォームスタートのみとなります。200 ~ 300 ミリ秒以内に応答する必要がある API など、低レイテンシーのワークロードに最適です。

また、予測可能なスパイクに適しています。同期的に呼び出される各 Lambda 関数は、すべての関数の合計同時実行数がアカウントの同時実行数の上限に達するまで、10 秒ごとに 1,000 回の同時実行回数でスケールしますが、Provisioned Concurrency を使っていると事前暖機されている範囲ではそのスケール速度を超えてバーストします。

SnapStart は、予測不可能なスパイクのあるワークロードで大きな価値を発揮します。例えば、通常は線形的な呼び出しパターンであっても、1 日の特定の時間帯に 1000 回の呼び出しが発生するような場合です。このようなシナリオでは、SnapStart がコールドスタート時間を改善できます。

なお先程ご体感いただいたように、SnapStart はコールドスタートの場合にスナップショットからの復元となり、わずかですがウォームスタートに比べるレイテンシーが高くなります。


モニタリング

Amazon CloudWatch、AWS X-Ray、Telemetry API を使って SnapStart のモニタリングができます。詳細は こちらのドキュメント をご参照ください。


利用費用

最後に SnapStart で発生する利用費用 についてご説明します。
利用費用は 2 種類あり、キャッシュの費用とリストアの費用が生じます。

  • キャッシュ
    • スナップショットの容量と保持期間によって発生します。関数バージョンが増える度に容量が増えますので、未使用の関数バージョンを削除いただければ SnapStart のキャッシュの利用費用を削減できます。
  • リストア
    • リストアの発生回数に依存します。なお、リストアは SnapStart を有効にしていれば毎回の実行で発生するというわけではなく、コールドスタートの場合のみ発生します。

SnapStart はこれら追加の利用費用がかかりますが、通常、コールドスタートは呼び出しの 1% 未満で発生 します。つまりリストアでSnapStart の利用費用が発生するといっても 99% タイルの呼び出しでは課金されない、ということと、SnapStart によって、従来の SnapStart を使わない場合のコールドスタートの度に行われていた初期化処理の時間分、つまりもともとの関数の Invocation の利用料金分が削減されますので、実行回数が多いワークロードなどは SnapStart を利用したほうがトータルではコストが安くなることがあります。


まとめ

実際に手を動かしながら、SnapStart の仕組みを学びました。

通常、関数コードを変更することなく、複雑なパフォーマンス最適化を実装したりすることなく、わずか 1 秒未満の起動パフォーマンスを提供できる SnapStart ですが、後半に記載したような考慮点も意識しつつ実装していただけたらと思います。


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

渡辺 紘久
アマゾン ウェブ サービス ジャパン合同会社
エンタープライズサポート テクニカルアカウントマネージャー

テクニカルアカウントマネージャー (TAM: タム) としてエンタープライズのお客様の AWS 活用支援を担当しています。
前回記載した記事 では、電子ドラムをいつか購入すると記載しておりましたが、先日無事に購入し、現在は"タム" 回しの上手い "TAM" を目指して日々練習に励んでいます。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する