AWS Lambda のタイムアウトエラーを根本解決する実装パターン

AWS Lambda は実行時間に制限があり、デフォルトでは3秒、最大15分のタイムアウト制限があります。本記事では、タイムアウトが発生する具体的な原因と、本番環境で即座に適用できる対策コードを紹介します。

Lambda タイムアウトが発生する4つの主要な原因

タイムアウトエラーは単なる処理が遅いだけではなく、根本的な構成の問題に起因することが多いです。まずは原因を特定することが重要です。

1. 外部 API 呼び出しの応答待ち

データベースやサードパーティ API へのリクエストが想定より遅延するケースです。ネットワークレイテンシー、API サーバーの過負荷、タイムアウト設定の不適切さが主な要因です。

2. VPC 内のリソースへのアクセス遅延

Lambda が VPC 内に配置されている場合、ENI(Elastic Network Interface)の割り当てに時間がかかることがあります。これは初回実行時やコンカレンシー増加時に顕著です。

3. メモリ不足による処理速度の低下

Lambda のメモリ割り当てが低いと、CPU 性能も比例して低下し、処理時間が延長されます。特に画像処理や大規模ファイル処理で顕著です。

4. コード内の無限ループやデッドロック

バグにより、処理が完了せずに実行時間が延び続ける場合です。ログ出力がない「沈黙するハング」状態になることもあります。

実装レベルでのタイムアウト対策コード

タイムアウト値の適切な設定

CloudFormation または AWS SAM を使用して、Lambda 関数のタイムアウト値を明示的に設定します。デフォルトの3秒では、ほとんどの実務用途に不十分です。


# CloudFormation テンプレート (YAML)
Resources:
  MyLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: my-timeout-function
      Runtime: python3.12
      Handler: index.lambda_handler
      Timeout: 60  # 60秒に設定
      MemorySize: 512  # メモリも十分に割り当てる
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              return {"statusCode": 200}
  

外部 API 呼び出しにタイムアウトを設定する

HTTP リクエスト自体にタイムアウトを設定しないと、Lambda タイムアウトまで無限に待機し続けます。以下は Python での実装例です。


import requests
import json
from datetime import datetime

def lambda_handler(event, context):
    """
    外部APIへのリクエストでタイムアウトを適切に設定する例
    """
    try:
        # connect_timeout: 接続確立の時間制限(秒)
        # timeout: 全体のタイムアウト(秒)
        response = requests.get(
            'https://api.example.com/data',
            timeout=(5, 10),  # (接続タイムアウト, 読み込みタイムアウト)
            headers={'User-Agent': 'AWS-Lambda'},
            params={'key': event.get('query_param')}
        )
        response.raise_for_status()
        
        return {
            'statusCode': 200,
            'body': json.dumps(response.json())
        }
    
    except requests.exceptions.Timeout:
        # タイムアウトエラーは明示的にハンドルする
        print(f"API タイムアウト発生: {datetime.now().isoformat()}")
        return {
            'statusCode': 504,
            'body': json.dumps({'error': 'API request timeout'})
        }
    
    except requests.exceptions.RequestException as e:
        print(f"API エラー: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps({'error': 'API request failed'})
        }
  

非同期処理とビジー待ちの回避

長時間の処理は Lambda 内で完結させず、SQS や Step Functions を使用して非同期に分割します。以下は Lambda から SQS にメッセージを送信し、別の Lambda で処理する例です。


import boto3
import json
from datetime import datetime

sqs_client = boto3.client('sqs')

def lambda_handler(event, context):
    """
    重い処理をSQSキューに送信して非同期実行する
    """
    queue_url = 'https://sqs.ap-northeast-1.amazonaws.com/123456789/MyQueue'
    
    try:
        # 処理対象のデータをSQSに送信
        message_body = {
            'task_id': event.get('task_id'),
            'data': event.get('payload'),
            'timestamp': datetime.now().isoformat()
        }
        
        response = sqs_client.send_message(
            QueueUrl=queue_url,
            MessageBody=json.dumps(message_body)
        )
        
        return {
            'statusCode': 202,  # Accepted
            'body': json.dumps({
                'message': 'Task queued for processing',
                'messageId': response['MessageId']
            })
        }
    
    except Exception as e:
        print(f"SQS送信エラー: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps({'error': 'Failed to queue task'})
        }
  

Context オブジェクトを活用した残り時間チェック

Lambda の context オブジェクトには、残りの実行時間を取得できる get_remaining_time_in_millis() メソッドがあります。これを使い、タイムアウト直前に処理を中断できます。


import json
import time

def lambda_handler(event, context):
    """
    残り時間を確認しながら処理を実行する
    """
    
    # Lambda 関数の設定タイムアウト: 60秒とする
    MAX_EXECUTION_TIME_MS = 55000  # 55秒(余裕を持たせる)
    
    items_to_process = range(100)
    processed_items = []
    
    try:
        for item in items_to_process:
            # 5秒ごとに残り時間をチェック
            remaining_time = context.get_remaining_time_in_millis()
            
            if remaining_time < MAX_EXECUTION_TIME_MS:
                print(f"タイムアウト間近: 残り時間 {remaining_time}ms")
                # 処理を中断して結果を返す
                break
            
            # 実際の処理(ここでは1秒待機をシミュレート)
            print(f"アイテム {item} を処理中...")
            time.sleep(1)
            processed_items.append(item)
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'processed_count': len(processed_items),
                'total_count': len(list(items_to_process)),
                'message': 'Processing completed or interrupted gracefully'
            })
        }
    
    except Exception as e:
        print(f"処理エラー: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }
  

VPC 内実行時のタイムアウト問題への対策

Lambda を VPC 内で実行すると、ENI 割り当ての遅延が発生します。以下の対策を実装してください。

Lambda 用の VPC エンドポイント設置

AWS のマネージドサービス(S3、DynamoDB など)には VPC エンドポイントを経由してアクセスし、インターネットゲートウェイを経由したルーティングを避けます。


# CloudFormation テンプレート (YAML)
Resources:
  S3VPCEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: vpc-xxxxx
      ServiceName: com.amazonaws.ap-northeast-1.s3
      RouteTableIds:
        - rtb-xxxxx
      PolicyText:
        Statement:
          - Effect: Allow
            Principal: '*'
            Action: 's3:*'
            Resource: '*'
  

メモリとコンカレンシー設定の最適化

メモリを1024MB 以上に設定し、Reserved Concurrency を設定して ENI 割り当てを予約します。


# AWS CLI コマンド
aws lambda update-function-configuration \
  --function-name my-vpc-function \
  --memory-size 1024 \
  --timeout 60

# コンカレンシー予約の設定
aws lambda put-function-concurrency \
  --function-name my-vpc-function \
  --reserved-concurrent-executions 10
  

デバッグと監視の実装

CloudWatch ログでの詳細なトレーシング

タイムアウト発生時の原因特定のため、処理の各ステップでログ出力を行います。


import logging
import json
from datetime import datetime

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    """
    詳細ログ出力を含む実装
    """
    start_time = datetime.now()
    
    logger.info(f"Lambda 実行開始: 関数名={context.function_name}, 要求ID={context.request_id}")
    logger.info(f"設定タイムアウト: {context.function_version}秒, メモリ: {context.memory_limit_in_mb}MB")
    
    try:
        logger.info(f"ステップ1: データ取得開始")
        # 処理A
        time_after_step1 = (datetime.now() - start_time).total_seconds()
        logger.info(f"ステップ1完了: {time_after_step1:.2f}秒経過")
        
        logger.info(f"ステップ2: API呼び出し開始")
        # 処理B
        time_after_step2 = (datetime.now() - start_time).total_seconds()
        logger.info(f"ステップ2完了: {time_after_step2:.2f}秒経過")
        
        logger.info("処理完了")
        return {'statusCode': 200, 'message': 'Success'}
    
    except Exception as e:
        elapsed = (datetime.now() - start_time).total_seconds()
        logger.error(f"エラー発生: {str(e)}, 経過時間: {elapsed:.2f}秒")
        raise
  

よくある質問

A: デフォルトは 3 秒、最大は 15 分(900 秒)です。本番運用では最低でも 30~60 秒の設定を推奨します。API 呼び出しを含む場合は 120 秒以上の余裕を持たせてください。

A: どちらも同じタイムアウト現象を示しています。CloudWatch ログに「Task timed out after」というメッセージが出力される場合は、Lambda の実行時間がタイムアウト値を超えたことを意味します。

A: 以下の順で対策してください:(1) メモリを 1024MB 以上に増やす、(2) Reserved Concurrency を設定する、(3) VPC エンドポイントを設置する、(4) 処理を非同期化する。これらで 90% 以上のケースが解決します。

まとめ

  • Lambda タイムアウトは「外部 API 待機」「VPC ENI 割り当て遅延」「メモリ不足」「無限ループ」の 4 つが主要原因
  • HTTP リクエストには必ず timeout パラメータを設定し、Lambda 内の無限待機を防ぐ
  • context.get_remaining_time_in_millis() で残り時間を監視し、グレースフルシャットダウンを実装する
  • VPC 内実行時はメモリを 1024MB 以上に、Reserved Concurrency を設定して ENI 割り当て遅延を回避する
  • 長時間処理は SQS や Step Functions で非同期化し、Lambda 内では完結させない
  • CloudWatch ログで各ステップの実行時間を記録し、ボトルネックを可視化する

← 前の記事LLMのハルシネーション対策:実装可能な5つの有効な方法
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →