LambdaのAWS請求が1.4倍になって本気で焦った話|Power Tuningで月30万円削減できた
「サーバーレスって安いんじゃないの?」と思ってたら請求書が爆増した経験ありませんか?半年間のPower Tuning本番運用で気づいたメモリ設定の落とし穴と、実際に効いたコスト削減の記録です。
Lambda の請求書を見て本気でやばいと思った話
半年前、月次のAWS請求を確認していたら、Lambda費用が前月比で1.4倍に膨れ上がっていた。「サーバーレスって安いんじゃないの?」という感覚で雑に運用してきたツケが一気に来た感じで、正直焦った。
うちのチームは当時、マイクロサービスのほぼ全処理をLambdaに乗せており、日次で数百万インボケーションが走っている状態だった。各Lambdaのメモリ設定はほぼ「デフォルト128MB」か「とりあえず1024MB」のどちらかで、誰もちゃんと測定していなかった。これが問題の根本だった。
で、そこから半年かけてLambda Power Tuningを中心に課金最適化を徹底的にやり込んで、結果として月30万円ほどの削減に成功した。実装の流れと気づきをまとめておきたい。Lambda Cold Startの対策については以前書いた「Lambda Cold Start地獄から脱出した|本番で効いた5つの対策」も読んでもらえると、今回の話と組み合わせて使いやすいはず。
Lambda課金の仕組みをちゃんと理解する(意外と盲点がある)
まず前提として、2026年時点でのLambda課金モデルを改めて整理しておく。
Lambda の課金は大きく リクエスト数 と 実行時間(GB-秒) の2軸で構成される。
| 項目 | 料金 | 備考 |
|---|---|---|
| リクエスト数 | $0.20 / 100万リクエスト | 最初の100万は無料枠 |
| 実行時間(x86) | $0.0000166667 / GB-秒 | 最初の400,000 GB-秒は無料枠 |
| 実行時間(ARM/Graviton2) | $0.0000133334 / GB-秒 | x86比で約20%安い |
ここで気づきにくい点が一つある。実行時間はメモリサイズと連動しているという事実だ。
128MBで10秒走るFunctionと、1024MBで1秒走るFunctionは、GB-秒ベースでは同じ1.28GB-秒になる。つまり、メモリを増やして実行時間が短くなるなら、トータルコストは変わらないか、むしろ下がる可能性がある。「メモリ少ない方が安い」という思い込みのままにしておくと、地味にずっと損し続けることになる。
さらに2025年末から一部リージョンで提供が本格化した Lambda Tiered Pricing の影響も無視できない。月間インボケーション数が一定以上になると段階的に単価が下がる仕組みで、うちのような大量インボケーションのケースでは料金計算の複雑さが増している。
# 現在のLambda関数のメモリ設定を一括確認するAWS CLIコマンド
aws lambda list-functions \
--query 'Functions[*].{Name:FunctionName, Memory:MemorySize, Runtime:Runtime}' \
--output table
これを実行したとき、自分のアカウントで128MBと1024MBしか存在しないという事実が可視化されて、若干引いた。
Lambda Power Tuningで実際に計測した結果
Lambda Power Tuningは、AWSが公式に提供しているOSS(Step Functions + Lambdaで構成されたSAMアプリ)で、指定したLambda関数を異なるメモリサイズで並列実行して、コストと速度のトレードオフを計測してくれるツールだ。手動でプロファイリングする手間を丸ごと自動化してくれる感じで、最初に触ったときは「これ最初から使えばよかった」と思った。
まず対象にしたのは、日次バッチで動く「S3からデータ取得→変換→DynamoDB書き込み」を行うFunction。このFunctionはそれまで512MBで設定していた。
# SAMでPower Tuningをデプロイ
aws cloudformation create-stack \
--stack-name lambda-power-tuning \
--template-url https://s3.amazonaws.com/amazon-src/lambda/power-tuning/latest/template.yml \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
# 実行ペイロードを作成
cat > input.json << 'EOF'
{
"lambdaARN": "arn:aws:lambda:ap-northeast-1:123456789012:function:my-batch-function",
"powerValues": [128, 256, 512, 1024, 2048, 3008],
"num": 50,
"payload": {"source": "s3", "bucket": "my-bucket"},
"parallelInvocation": true,
"strategy": "balanced"
}
EOF
# Step Functionsを起動
aws stepfunctions start-execution \
--state-machine-arn arn:aws:states:ap-northeast-1:123456789012:stateMachine:powerTuningStateMachine \
--input file://input.json
計測結果をビジュアル化すると、こういう感じになった(実測値に基づく):
xychart-beta
title "Lambda メモリ別 コスト vs 実行時間"
x-axis [128MB, 256MB, 512MB, 1024MB, 2048MB, 3008MB]
y-axis "コスト指数(512MB=1.0基準)" 0 --> 2.5
bar [2.4, 1.3, 1.0, 0.68, 0.62, 0.85]
line [0.5, 0.7, 1.0, 1.4, 1.8, 2.3]
※ 棒グラフがコスト指数、折れ線が相対的な実行時間スコア(高いほど遅い)
この結果を見て正直驚いたのが、1024MBがコスト最安値だったこと。128MBは遅すぎてGB-秒コストが跳ね上がり、3008MBはメモリが余って逆にコスト増という結果だった。512MBという当初設定は「なんとなく中間」で選んでいただけで、全然最適ではなかったわけだ。数字を見るまで「512MBで問題ないでしょ」と思い込んでいたのがちょっと恥ずかしかった。
strategyの選び方は思ったより重要
Power Tuningのstrategyパラメータは3種類ある。最初は全部balancedで計測してたんだけど、これが微妙に間違いだった。
| strategy | 最適化の方向 | 向いているケース |
|---|---|---|
cost | コスト最小化優先 | バッチ処理、非同期処理 |
speed | 実行時間最小化優先 | APIバックエンド、低遅延要件 |
balanced | コストと速度のバランス | ほとんどのケースで推奨 |
APIバックエンドのFunctionにはspeedを使った方が実際のユーザー体験に合った最適化ができることに気づいて、途中から方針を変えた。ユーザーが待つFunctionとバックグラウンドで動くFunctionは、別の戦略で最適化すべきだと今は思っている。好みが分かれる話かもしれないけど、個人的には「全部balanced」は雑だったと反省している。
本番で構築した最適化パイプラインの全体構成
手動でPower Tuningを都度実行するのは現実的じゃないので、CI/CDに組み込んだ。構成はこんな感じ:
graph TB
subgraph Developer_Flow ["開発フロー"]
DEV["開発者
コード変更"]
PR["Pull Request"]
end
subgraph CICD ["CodePipeline (CI/CD)"]
BUILD["CodeBuild
ユニットテスト・ビルド"]
DEPLOY_STG["Staging環境
デプロイ"]
TUNING["Lambda Power Tuning
自動実行 (Step Functions)"]
REPORT["コスト最適化レポート
生成・S3保存"]
APPROVAL["手動承認ゲート
(推奨設定の確認)"]
DEPLOY_PROD["Production環境
デプロイ"]
end
subgraph MONITORING ["モニタリング"]
CW["CloudWatch
Lambda Insights"]
COST_EXP["Cost Explorer
AnomalyDetection"]
SLACK["Slack通知
コスト異常アラート"]
end
subgraph AWS_ARCH ["AWSリソース"]
subgraph VPC_PROD ["VPC (Production)"]
subgraph AZ_A ["AZ-a"]
LAMBDA_A["Lambda Functions
(最適化済みメモリ)"]
SQS_A["SQS Queue"]
end
subgraph AZ_B ["AZ-b"]
LAMBDA_B["Lambda Functions
(最適化済みメモリ)"]
SQS_B["SQS Queue"]
end
end
DDB["DynamoDB"]
S3["S3 Buckets"]
ARM["Graviton2 ARM
(20%コスト削減)"]
end
DEV --> PR --> BUILD --> DEPLOY_STG
DEPLOY_STG --> TUNING --> REPORT --> APPROVAL --> DEPLOY_PROD
DEPLOY_PROD --> LAMBDA_A & LAMBDA_B
LAMBDA_A & LAMBDA_B --> DDB & S3
LAMBDA_A & LAMBDA_B -.-> ARM
CW --> COST_EXP --> SLACK
LAMBDA_A & LAMBDA_B --> CW
ポイントは「承認ゲート」を設けたこと。Power Tuningの推奨値をそのまま自動適用するのは最初は怖かったので、Slackに結果を通知して人間が確認してからProduction適用する仕組みにした。3ヶ月運用してからようやく信頼できるようになって、一部の定型的なFunctionは完全自動化した。焦って全自動にしなくてよかったと今でも思っている。
ARM(Graviton2)への移行でさらに20%削減
Power Tuningでメモリを最適化した後に気づいたのが、ARMアーキテクチャへの移行だ。対応している場合、x86と比較して約20%コストが下がる。移行手間の割に効果が持続するので、個人的にはこれが一番コスパの良い施策だった。
import boto3
def migrate_to_arm(function_name: str) -> dict:
client = boto3.client('lambda')
# 現在の設定確認
current = client.get_function_configuration(FunctionName=function_name)
current_arch = current.get('Architectures', ['x86_64'])[0]
if current_arch == 'arm64':
return {'status': 'already_arm', 'function': function_name}
# ARM移行(Pythonランタイムは変更不要)
response = client.update_function_configuration(
FunctionName=function_name,
Architectures=['arm64']
)
return {
'status': 'migrated',
'function': function_name,
'architecture': 'arm64',
'expected_cost_reduction': '~20%'
}
# 移行対象のFunctionをリストアップして一括移行
if __name__ == '__main__':
lambda_client = boto3.client('lambda')
functions = lambda_client.list_functions()['Functions']
python_functions = [
f['FunctionName'] for f in functions
if 'python' in f.get('Runtime', '')
]
for fn in python_functions:
result = migrate_to_arm(fn)
print(f"{result['function']}: {result['status']}")
ただし注意点がある。ネイティブ拡張ライブラリ(C拡張)を含むPythonパッケージを使っている場合、ARM向けにビルドし直す必要がある。うちではnumpyとpillowを使っているFunctionがあって、そこだけ依存関係を整理する手間がかかった。numpy系はDockerベースのビルド環境を使えばわりとスムーズに移行できる。
# ARM向けビルド用Dockerfile
FROM --platform=linux/arm64 public.ecr.aws/lambda/python:3.13-arm64
COPY requirements.txt .
RUN pip install -r requirements.txt --target /var/task --platform manylinux2014_aarch64 --only-binary=:all:
COPY src/ /var/task/
CMD ["handler.lambda_handler"]
6ヶ月運用してわかった、コスト削減効果の実績
最高で108万円だったLambda費用が、6ヶ月後には78万円まで落ちた。削減率としては28%ほどで、目標の30%にはもう少し届いていないけど、まだ最適化しきれていないFunctionが残っているので、続けていけばもう少し下がると思っている。
xychart-beta
title "月別Lambda費用推移(万円)"
x-axis ["1月", "2月", "3月", "4月", "5月", "6月"]
y-axis "費用(万円)" 0 --> 120
bar [108, 105, 92, 78, 72, 78]
line [108, 105, 92, 78, 72, 78]
取り組み別の削減効果(推定)はざっくりこんな感じ:
| 施策 | 削減効果 | 工数 |
|---|---|---|
| Power Tuningでメモリ最適化 | 約15% | 中(パイプライン構築含む) |
| ARMアーキテクチャ移行 | 約10% | 小〜中 |
| 不要Functionの整理・廃止 | 約5% | 小 |
| Provisioned Concurrency見直し | 約3% | 小 |
一番コスパが良かったのはやっぱりARM移行だった。Power Tuningほどの劇的な改善はないけど、移行作業自体は数時間で終わって、その後はずっと20%引きが続く。これはやっておかない理由がない施策だと思う。
Provisioned Concurrencyについては、SnapStartとの組み合わせも検討中だ。Lambda SnapStart 2026年実装ガイドで詳しく書いているので、冷却問題と合わせて読んでほしい。Lambda以外のAWSサービス全体のコスト最適化という文脈なら、Savings Plans vs Reserved Instancesもアカウント全体での最適化判断の参考になるはず。
まとめ
半年間やってきてわかったことを整理しておく。
-
デフォルトメモリ設定のまま放置は確実に損している。128MBは「安い」のではなく「遅くてGB-秒が増える」ので、実は高くなるケースが多い。まずPower Tuningで現状を計測するところから始めるのが一番手っ取り早い。
-
Power TuningのstrategyはAPIと非同期で分けるべき。全部
balancedにするのは雑だった。ユーザーが待つFunctionはspeed優先、バックグラウンドバッチはcost優先で考えた方がいい。 -
ARMへの移行は工数が少ない割に効果が持続する。Python・Node.js・Javaはわりとスムーズに移行できた。ネイティブ拡張がなければ1時間以内で終わる。
-
自動化しないと続かない。手動でPower Tuningを回すのは最初だけで、CI/CDへの組み込みが本質的に重要。承認ゲートを設けた段階的自動化がバランス良かった。
-
コスト削減の数字は思ったより出る。「サーバーレスは安い」という思い込みを捨てて、ちゃんと計測してみると改善余地が見つかることが多い。
次のアクション:まずはaws lambda list-functionsで現在のメモリ設定を確認して、128MBか1024MBに偏っていないかチェックすることから始めてみてほしい。そこから気になったFunctionを一つPower Tuningにかけてみると、数字が面白いほど見えてくる。
皆さんのチームではLambdaのメモリ設定ってどう決めてます? なんとなく運用していたり、逆にすでに徹底管理していたりと様々だと思うけど、ぜひ参考にしてもらえれば。