2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

# AWS Lambdaの実行の仕組みを理解する

2
Posted at

AWS Lambdaの実行の仕組みを理解する

はじめに

こんにちは。株式会社シーイーシーのAWSコミュニティチームです。

今回は、2026 Japan AWS Jr. Championsの荒木が担当いたします。

本記事では、AWS Lambdaの実行の仕組みについて、実行環境のライフサイクルを軸にご説明します。

AWS Lambdaは、サーバーの管理なしにコードを実行できるサーバーレスコンピューティングサービスです。コードの実行において、手軽に使える一方で、次のような挙動に戸惑った経験がある方も多いのではないでしょうか。

  • 初回だけレスポンスが遅い
  • グローバル変数に前回の呼び出しの値が残っていた
  • 急にスロットリングされた

これらの挙動には、Lambdaが実行環境を作成・再利用する仕組みが関係しています。
そこで本記事では、AWS Lambdaの実行の仕組みについて、実行環境のライフサイクルを軸にご説明します。目次は次の通りです。
※なお、本記事では、リレーショナルデータベースを、「RDB」と表記します。

  1. Lambdaの「関数」と「実行環境」の違い
  2. 実行環境のライフサイクル
  3. 実行環境の再利用とグローバル変数
  4. コールドスタートとウォームスタート
  5. 実行環境の増加とRDB接続
  6. 同時実行(Concurrency)とスロットリング
  7. メモリー・CPU・課金の関係

前提条件

  • 対象読者:AWS Lambdaで関数を作成した経験がある方
  • コード例:Python
  • 実測環境:東京リージョン
  • 記載したサービス仕様:2026年8月時点

実行環境の基本概念を中心に解説します。ランタイム固有の内容や実測条件は、該当箇所に明記しています。

それでは、Lambda関数と実行環境の違いから見ていきます。

1. Lambdaの「関数」と「実行環境」の違い

ここでいう「関数」は、利用者が作成した関数コードと設定を指します。一方、「実行環境」は、そのコードを実際に動かすためにLambdaが用意する場所です。

実行環境は、関数コードを動かすためにLambdaが用意する、ほかの環境から隔離された場所です。実行環境には、Pythonなどのランタイムや、関数に割り当てたメモリー、一時ファイルを保存する領域などが含まれます。利用者が実行環境を支えるサーバーを管理する必要はありません。

作成された実行環境は、1回の呼び出しで必ず破棄されるわけではありません。後続のリクエストに備えて保持され、再利用されることがあります。この再利用の仕組みが、グローバル変数の値の持ち越しやウォームスタートに関係します。

2. 実行環境のライフサイクル

関数コードと実行環境の違いを確認したところで、実行環境が作成されてから破棄されるまでの流れを見ていきます。

実行環境のライフサイクルは、大きく3つのフェーズに分かれます。

[Init] --> [Invoke] --> [Invoke] --> ... --> [Shutdown]
 初期化      呼び出し     呼び出し              終了
 (最初の1回)  (毎回)       (再利用)            (破棄時)

2.1 Init(初期化フェーズ)

Initフェーズは、通常、新しい実行環境を準備するときに実行されます。通常、同じ実行環境が再利用される間、Initフェーズは繰り返されません。ただし、実行エラー後に環境がリセットされた場合などは、再度実行されることがあります。主な処理は以下の通りです。

  1. 拡張機能の起動
  2. ランタイムの起動(Pythonなどの言語ランタイム)
  3. 関数の初期化コードの実行(ハンドラ関数の外側に書いたimport文やグローバル変数の初期化など)

Lambdaの拡張機能(Lambda Extensions)は、監視ツールとの連携など、関数の処理を補助するための仕組みです。拡張機能もInitフェーズで初期化されるため、処理内容によってはコールドスタート時間に影響します。

2.2 Invoke(呼び出しフェーズ)

ハンドラ関数が実際に呼び出されるフェーズです。通常、実行環境が再利用される場合は、Initフェーズを経ずにInvokeフェーズが実行されます。その分、初回より短時間で処理を開始できます。

以下のコード例では、どの処理がInitフェーズとInvokeフェーズで実行されるのかをコメントで示しています。

import boto3  # Initフェーズで実行される

# ハンドラ外:Initフェーズで一度だけ実行される
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Users")

def handler(event, context):
    # Invokeフェーズのたびに実行される
    return table.get_item(Key={"id": event["id"]})

2.3 Shutdown(終了フェーズ)

実行環境は、一定期間使用されなかった場合などにLambdaによって破棄されます。破棄される時期は公表されておらず、利用者からは制御できません。

呼び出しが続いている場合でも、ランタイム更新やメンテナンスに伴って実行環境が再作成されることがあります。そのため、継続的に呼び出しても同じ実行環境が使われ続けるとは限りません。

3. 実行環境の再利用とグローバル変数

前のセクションでは、1つの実行環境が複数の呼び出しに再利用されることを説明しました。この再利用は初期化時間を省ける一方、実行環境内の状態が次の呼び出しにも引き継がれる場合があります。

ハンドラ外で定義した変数は、同じ実行環境で処理される限り、前回の呼び出しの値を保持したまま次の呼び出しに引き継がれます。

call_count = 0  # ハンドラ外で定義したグローバル変数


def handler(event, context):
    global call_count
    call_count += 1
    return {"call_count": call_count}

この関数を連続で呼び出すと、同じ実行環境が再利用されている間はcall_countが1、2、3と増え続けます。一方、コールドスタートで新しい実行環境が作成されると、また1に戻ります。ただし、実行環境は複数作成されることがあり、call_countは環境ごとに別々の値を持ちます。毎回0から始まる、またはすべての呼び出しで1つの値を共有すると想定して実装すると、不具合につながります。

避けるべきなのは、リクエスト固有のデータをグローバル変数に置くことです。たとえば、ユーザーIDや認証情報をグローバルに保持すると、後続のリクエストで、前回処理したユーザーの値を誤って参照する危険があります。呼び出しごとにプロセスを作り直すテストでは再現しにくいため、同じプロセスでハンドラを複数回呼び出すテストも用意しておくとグローバル変数に残った値が引き起こす不具合を検出しやすくなります。

使い分けの指針は次の通りです。

  • グローバル(ハンドラ外)に置いてよいもの:SDKクライアント、設定値など、リクエスト間で共有しても安全なもの。RDB接続を再利用する場合は、切断時の再接続も考慮する
  • ハンドラ内のローカル変数で扱うもの:ユーザーID、リクエストの入力値、処理の途中結果など、リクエスト固有のもの

4. コールドスタートとウォームスタート

実行環境の再利用は、変数の状態だけでなく、処理開始までの時間にも影響します。ここでは、コールドスタートとウォームスタートの違いを説明します。

  • コールドスタート:新しい実行環境を準備してから、ハンドラを実行する呼び出し。実行前に、コードのロードやランタイム、拡張機能、ハンドラ外コードの初期化が行われる
  • ウォームスタート:既存の実行環境を再利用し、ハンドラを実行する呼び出し

実際の挙動はAmazon CloudWatch Logsで確認できます。次のキャプチャーは、同じ関数(Pythonランタイム、メモリー128MB、boto3をimportする最小構成)を3回連続で呼び出した際のログです。

CloudWatch LogsのREPORT行。1回目の呼び出しのみInit Durationが記録され、2回目以降には記録されていない

  • 初回(コールドスタート):REPORT行にInit Duration: 492.50 msが記録され、課金対象時間はBilled Duration: 495 ms
  • 2回目以降(ウォームスタート):Init Durationの記載がなく、課金対象時間はBilled Duration: 2 ms

ハンドラ本体の処理時間(Duration)はどちらも約1.5ミリ秒で変わりません。一方、初回だけboto3のimportを含む初期化に約500ミリ秒かかっています。REPORT行から、コールドスタート時にInitフェーズの時間が加わったことを確認できます。

コールドスタートで時間がかかる主な要因は次の通りです。

要因 内容
パッケージサイズ 読み込むコードや依存関係が多いほど、初期化に時間がかかる場合がある
初期化コード 大きなライブラリーのimportや接続確立など、ハンドラ外の処理時間が加わる
VPC経由の通信 接続先やネットワーク構成によって、初回接続に時間がかかる場合がある
ランタイム 言語によって起動コストが異なる

コールドスタートを減らすには

  • 初期化コードを軽くする:使用するものだけをimportし、重い処理は必要になるまで遅延させる
  • 読み込むコードを減らす:不要な依存関係を削除し、必要なモジュールのみをパッケージ化する。Lambda Layerは依存関係の共有や管理には有効だが、Layerへの移行だけで初期化時間が短くなるとは限らない
  • Provisioned Concurrency(プロビジョニング済み同時実行):指定した数の実行環境を、あらかじめ初期化した状態で確保する。起動時間を安定させられるが、確保中も料金が発生する
    • 詳しくは「6. 同時実行(Concurrency)とスロットリング」で説明
  • 定期的な呼び出し:定期実行によって結果的にウォームな実行環境が再利用される場合はあるが、同じ環境が維持される保証はない。また、急な同時実行数の増加には対応できない。起動レイテンシを要件として保証したい場合は、Provisioned Concurrencyを利用する
  • Lambda SnapStart:初期化済みのスナップショットから復元して起動を高速化する(対応ランタイムで有効)

5. 実行環境の増加とRDB接続

ここまでは、実行環境が再利用された場合の挙動を見てきました。一方、アクセスが増えると、Lambdaは複数の実行環境を用意してリクエストを処理します。ここでは、実行環境の増加がRDB接続に与える影響を説明します。

特に影響が表れやすいのが、LambdaからRDBへ接続する構成です。Amazon Relational Database Service(Amazon RDS)へ直接接続し、各実行環境がRDB接続を保持する構成では、実行環境の増加に伴って接続数も増えます。

何が問題なのか

RDBへの接続確立はハンドラ外に書き、実行環境ごとに再利用する方法があります。以下は接続再利用の考え方を示す簡略例です。get_password()は、AWS Secrets Managerなどから認証情報を取得する処理を別途実装しているものとします。

import pymysql

# ハンドラ外: Initフェーズで一度だけ接続を確立し、実行環境の再利用に乗せる
connection = pymysql.connect(
    host="mydb.xxxxx.ap-northeast-1.rds.amazonaws.com",
    user="app_user",
    password=get_password(),  # Secrets Managerなどから取得する想定
    database="mydb",
)

def handler(event, context):
    # Invokeフェーズでは確立済みの接続を使うだけ
    with connection.cursor() as cursor:
        cursor.execute("SELECT * FROM users WHERE id = %s", (event["id"],))
        return cursor.fetchone()

ウォームスタートでは、接続確立にかかる時間を省けます。ただし、実行環境ごとに接続を保持するため、同時実行数が増えるとRDB接続数も増加します。実運用では、再利用時に接続が有効かを確認し、切断されていれば再接続する処理も必要です。

  • 本記事で扱う標準のLambda関数では、1つの実行環境が同時に処理するリクエストは1件
  • 各実行環境がRDB接続を1本ずつ保持する構成では、同時実行数の増加に伴ってRDB接続数も増える
  • Lambdaは関数ごとに10秒あたり最大1,000実行環境という速度でスケールできる一方、RDB側の最大接続数(max_connections)はインスタンスサイズに制約された有限のリソース

同時実行の仕組みについては、後半の「6. 同時実行(Concurrency)とスロットリング」で説明します。

このスケーリング速度とRDBの最大接続数の差が、接続枯渇を招く要因になります。トラフィックが急増すると、Lambda側に同時実行の余裕があっても、先にRDB側が接続数の上限へ到達する可能性があります。

一方、ハンドラ内で毎回接続を確立して閉じる場合は、接続確立にかかる時間が毎回の処理に加わり、RDB側にも接続の作成と破棄による負荷がかかります。接続を再利用する場合は接続数が増えやすく、毎回接続する場合は処理時間とRDB側の負荷が増えるため、両方を考慮する必要があります。

Amazon RDS Proxyによる接続管理

RDB接続数の増加を抑える選択肢の1つがAmazon RDS Proxyです。LambdaとRDBの間で接続を管理するマネージドプロキシーで、次の仕組みによってRDBへの接続数の増加を抑えます。

  • 接続プール(コネクションプーリング):プロキシーがRDBへの接続を確立・維持し、複数のLambda実行環境から再利用する
  • 多重化(multiplexing):トランザクションの終了後、利用可能になったRDB接続をほかのクライアントへ割り当てる。これにより、Lambdaの実行環境数とRDBへの実接続数を分けて管理できる

アプリケーションから利用するには、必要な認証設定を行い、接続先をDBインスタンスのエンドポイントからRDS Proxyのエンドポイントへ変更します。RDS Proxy側では、認証情報、VPC、サブネット、セキュリティグループなどの設定が別途必要です。

一時テーブルや一部のセッション変数など、RDS Proxyが安全に共有できないセッション固有の状態を使用すると、クライアント接続が特定のRDB接続へピン留め(pinning)される場合があります。ピン留め中のRDB接続はほかのクライアントが再利用できないため、多重化の効率が下がります。対象となる操作はDBエンジンによって異なるため、利用するエンジンのピン留め条件を確認する必要があります。

Amazon DynamoDBを利用する選択肢

Amazon DynamoDBのようにAPI経由で利用するサービスでは、RDBのmax_connectionsに相当する接続数管理は不要です。リクエスト単位で処理するモデルはLambdaと組み合わせやすい一方、SDKクライアントの再利用やタイムアウト、サービス側のスロットリングは引き続き考慮します。

リレーショナルモデルやSQLが必要な場合は、Amazon RDS Proxyの利用を検討します。そうでない場合は、データモデルやアクセス方法を踏まえてAmazon DynamoDBなども候補にします。

6. 同時実行(Concurrency)とスロットリング

前のセクションでは、実行環境が増えるとRDB接続数も増えることを説明しました。実行環境がいくつ同時に動くかを表すのが、ここで扱う「同時実行数」です。同時実行数はRDB接続だけでなく、Lambdaが同時に処理できるリクエスト数やスロットリングにも関係します。

本記事で扱う標準のLambda関数では、1つの実行環境が同時に処理するリクエストは1件です。N件のリクエストを同時に処理するには、原則としてN個の実行環境が必要です。この「同時に動いている実行環境の数」が同時実行数(concurrency)です。

同時に3リクエスト --> 実行環境を3つ用意(それぞれコールドスタートの可能性)

同時実行数が上限に達すると、呼び出し方法に応じてスロットリングが発生します。

  • アカウント単位の同時実行上限:リージョンごとにデフォルトの上限がある(引き上げ申請が可能)
  • 上限に達した場合:Lambda Invoke APIを直接、同期呼び出しした場合は、429 TooManyRequestsExceptionが返る。非同期呼び出しでは、スロットリングされたイベントがLambdaの内部キューへ戻され、デフォルトでは最大6時間、間隔を延ばしながら再試行される。イベントソースマッピングの再試行動作は、イベントソースごとに異なる

同時実行を制御するオプションは次の2つです。

  • Reserved Concurrency(予約済み同時実行数):特定の関数に対して同時実行枠を確保する。ほかの関数に枠を使用されないよう確保すると同時に、その関数の同時実行数の上限としても機能する。設定自体に追加料金はかからない
  • Provisioned Concurrency(プロビジョニング済み同時実行):初期化済みの実行環境を指定した数だけ確保する。コールドスタート対策として使えるが、確保している間は関数を呼び出していなくても料金が発生する。設定値を超えた呼び出しにはオンデマンドの実行環境が使われ、その実行分は通常のオンデマンド料金で課金される

費用の違いをまとめると、次のようになります。

項目 Reserved Concurrency Provisioned Concurrency
設定に対する追加料金 なし あり
課金の基準 実際のリクエスト数と実行時間 設定した同時実行数、メモリー、有効時間に加え、実際のリクエスト数と実行時間
呼び出しがない時間 追加料金なし 確保中は料金が発生
設定値を超えた場合 関数がスロットリングされる Reserved Concurrencyなどの上限に達していなければ、オンデマンドの実行環境で処理される

Provisioned Concurrencyの確保時間は、有効にしてから無効にするまでを5分単位で切り上げて計算します。料金はメモリー設定、確保する同時実行数、利用リージョンなどで変わるため、具体的な金額はAWS Lambdaの料金ページで確認してください。

7. メモリー・CPU・課金の関係

同時実行数が「実行環境の数」に関する設定であるのに対し、メモリーは「1つの実行環境の性能」に関係します。最後に、メモリー設定がCPU性能、実行時間、料金へ与える影響を実測結果とともに確認します。

Lambdaでは、メモリー設定に応じてCPU性能も割り当てられます。メモリーを増やすと、CPU性能も比例して増えます。ネットワーク性能も構成や割り当て条件によって向上する場合がありますが、CPUと同じ条件で一律に比例するわけではありません。公式ドキュメント「Configure Lambda function memory」によると、1,769MBの設定で1vCPU相当のCPU性能が割り当てられます。

  • メモリーを増やすと、1リクエストあたりの処理が速くなる場合がある
  • 課金は「割り当てメモリー×実行時間(GB-秒)」で決まる

メモリー設定別の実行時間

実際にどの程度変わるのかを検証するため、CPUでの計算時間が処理時間を左右する、CPU負荷の高い処理で計測しました。SHA-256ハッシュの計算を50万回繰り返す次の関数を、コードは一切変えずメモリー設定だけを変えて実行した結果です(東京リージョン、Python 3.13、ウォームスタートで各3回実行した中央値)。

import hashlib
import time


def handler(event, context):
    # CPUバウンドな処理: SHA-256ハッシュを50万回繰り返す
    start = time.perf_counter()
    data = b"benchmark"
    for _ in range(500_000):
        data = hashlib.sha256(data).digest()
    elapsed_ms = (time.perf_counter() - start) * 1000
    return {"elapsed_ms": round(elapsed_ms, 1)}
メモリー設定 実行時間(ms) 処理速度(128MB比) GB-秒(課金の目安)
128MB 5,350 1.0倍 0.67
512MB 1,258 4.3倍 0.63
1,024MB 642 8.3倍 0.64
1,769MB 372 14.4倍 0.64
3,538MB 371 14.4倍 1.28

表のGB-秒は、割り当てメモリーとコード内で計測した実行時間から算出した比較用の値です。実際の料金には、Lambdaが記録する課金対象時間、リクエスト料金、CPUアーキテクチャーや料金階層などの条件が関係します。

今回の計測では、次の2点を確認できました。

  1. 128MBから1,769MBへ増やすと、実行時間は約14分の1になり、GB-秒はほぼ横ばい
    • メモリーの増加率と同程度に実行時間が短縮されたため
  2. 1,769MBから3,538MBへ増やしても、実行時間はほとんど変化なし
    • このコードは単一スレッドで動作するため、追加されたCPUリソースを活用できなかったと考えられる
    • 頭打ちになるメモリー設定はワークロードによって異なる

ただし、これはCPU負荷の高い処理の例です。処理時間の多くを外部APIやRDBからの応答待ちが占める関数では、メモリーを増やしても待ち時間は短くならないため、効果はワークロード次第です。

メモリーを増やした結果、実行時間が十分に短くなり、処理全体のコストが下がる場合もあります。「メモリーは小さいほど安い」とは限りません。

最適なメモリー設定はワークロードに依存するため、AWS Lambda Power Tuning(※1)などを利用し、実測結果をもとに決めるとよいでしょう。

※1 AWS Lambda Power Tuning:Lambda関数のメモリー設定を比較するためのオープンソースツールです。AWS Step Functionsを使って、対象の関数を複数のメモリー設定で繰り返し実行し、設定ごとの平均実行時間とコストを測定します。結果はグラフで表示でき、コスト、処理速度、または両者のバランスを基準にメモリー設定を比較できます。

計測は自分のAWSアカウント内で行われ、対象のLambda関数が実際に実行されます。そのため、外部APIの呼び出しやデータの書き込みを行う関数では、テスト用の入力データや検証環境を用意してから実行します。また、計測中に呼び出されるLambda関数とAWS Step Functionsには利用料金が発生します。

まとめ

本記事では、Lambdaの実行環境のライフサイクルを起点に、性能やRDB接続、同時実行数、コストとの関係を解説しました。冒頭に挙げた3つの挙動も、この仕組みで説明できます。初回だけレスポンスが遅いのはコールドスタートでInitフェーズが実行されるため、グローバル変数に前回の値が残るのは実行環境が再利用されるため、急なスロットリングは同時実行数が上限に達したためです。実装時に押さえておきたい点は次の4つです。

  • SDKクライアントなど、リクエスト間で安全に共有できるリソースはハンドラ外で初期化する。一方、ユーザーIDや認証情報など、リクエスト固有のデータはグローバル変数へ保存しない。RDB接続を再利用する場合は、切断時の再接続も考慮する
  • コールドスタート対策は、まず初期化処理と依存関係を見直す。安定した起動レイテンシが必要な場合は、Provisioned Concurrencyや対応ランタイムのLambda SnapStartを検討する
  • RDBへ接続する場合は、想定同時実行数とmax_connectionsを確認する。接続数の急増が見込まれる場合は、Amazon RDS Proxyを検討する
  • メモリー設定は実際のワークロードで計測して決める。今回のCPUバウンド処理では、メモリーを128MBから1,769MBへ増やすことで、GB-秒をほぼ変えずに実行時間を約14分の1へ短縮できた

メモリー設定を調整する際は、実行時間だけでなく、実行環境の再利用、同時実行数、GB-秒をあわせて確認してください。

参考リンク

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?