AWS S3 + CloudFront でキャッシュが更新されない・X-Cache: Miss from cloudfront が続く時の原因と対処法【Cache-Control / Invalidation】
公開日:2026-05-10/カテゴリ:AWS・CloudFront・インフラ/AnchorUp 技術ブログ
S3を更新したのにサイトが古いまま表示される
AWS S3 + CloudFront でウェブサイトをホスティングしていると、こんな状況に遭遇することがあります。
- S3 にファイルをアップロードしたのに、サイトが古いバージョンのままで表示される
- CloudFront のキャッシュをクリアしたはずなのに、まだ古いファイルが返ってくる
- Invalidation(無効化)をかけたのに変化がない
- 自分の環境では更新されているのに、ユーザーから「古い画面が出る」と言われる
- 逆に、
curlで何度確認してもX-Cache: Miss from cloudfrontが返り、キャッシュが効いていない
この問題は「どのレイヤーのキャッシュが原因か」を特定しないと解決できません。CloudFront のキャッシュ・ブラウザキャッシュ・S3 の設定ミスがそれぞれ絡み合うため、対処法を知らないと無駄に時間を使います。本記事では原因ごとに対処法を体系的に解説します。X-Cache: Miss from cloudfront の意味と、Miss が続く原因を知りたい方は次の次の章「X-Cache: Miss from cloudfront とは?」から読んでください。
仕組みの前提:CloudFront はどうキャッシュ時間(TTL)を決めるか
結論: TTL は「キャッシュポリシーの最小・デフォルト・最大 TTL」と「オリジン(S3)が返す Cache-Control / Expires」の組み合わせで決まります。 オリジンの max-age が優先されますが、最小 TTL〜最大 TTL の範囲に収められます。AWS 公式ドキュメント(Manage how long content stays in the cache)の表を整理すると次のとおりです。
| オリジン(S3)のヘッダー | 最小 TTL = 0 のとき | 最小 TTL > 0 のとき |
|---|---|---|
Cache-Control: max-age あり |
max-age と最大 TTL の小さい方 |
max-age を最小〜最大 TTL の範囲に収めた値 |
s-maxage と max-age の両方 |
CloudFront は s-maxage、ブラウザは max-age を使う |
同左(s-maxage を最小〜最大 TTL に収める) |
Expires のみ |
Expires の日時か最大 TTL の早い方 |
Expires を最小〜最大 TTL に収める |
| ヘッダーなし | デフォルト TTL | 最小 TTL とデフォルト TTL の大きい方 |
no-cache / no-store / private |
ヘッダーに従う(キャッシュしない) | 最小 TTL の間はキャッシュする |
キャッシュポリシーを使わない旧来の設定(レガシーキャッシュ設定)や、マネージドポリシー CachingOptimized のデフォルト TTL は 24 時間(86400 秒) です。max-age と Expires の両方がある場合は max-age だけが使われます。
これを知らずに「S3 を更新したから即座に反映されるはず」と思っていると、毎回ハマります。
X-Cache: Miss from cloudfront とは?毎回 Miss になる(Hit にならない)のはなぜか
結論: X-Cache: Miss from cloudfront は「リクエストを受けたエッジにキャッシュが無かったので、オリジン(S3 など)から取得して返した」という意味で、エラーではありません。 初回アクセスや TTL が切れた直後の Miss は正常です。同じ URL に続けてアクセスしても Miss が続く場合、原因はほぼ次の 3 つのどれかです。
- キャッシュポリシーが
CachingDisabledなど、TTL が 0 になっている - オリジンが
Cache-Control: no-cache/no-store/private/max-age=0を返していて、最小 TTL が 0 - クエリ文字列・Cookie・リクエストヘッダーがキャッシュキーに入っていて、リクエストごとに別のキャッシュになっている
X-Cache ヘッダーの値はそれぞれ何を意味するのか
CloudFront はレスポンスに X-Cache ヘッダーを付けます。値の意味は、アクセスログの x-edge-result-type 列の定義(AWS 公式ドキュメント Standard logging reference)と同じです。
| X-Cache の値 | 意味 | 対応が必要か |
|---|---|---|
Hit from cloudfront |
エッジのキャッシュから返した | 不要(キャッシュが効いている) |
RefreshHit from cloudfront |
キャッシュはあったが期限切れだったので、オリジンに最新か問い合わせてから返した | 基本は不要 |
Miss from cloudfront |
キャッシュに無かったので、オリジンから取得して返した | 2 回目以降も続くなら確認 |
Error from cloudfront |
4xx / 5xx のエラーを返した | 必要(オリジンや権限を確認) |
アクセスログではほかに LimitExceeded、CapacityExceeded、Redirect(HTTP→HTTPS のリダイレクト)なども記録されます。x-edge-detailed-result-type 列には、Origin Shield から返した時の OriginShieldHit も出ます。
まず確認:同じ URL に続けてアクセスしても Miss が続くか
1 回目が Miss なのは当然なので、同じ URL に 2〜3 回続けて GET し、2 回目以降に Hit になるかを見ます。curl -I は HEAD リクエストなので、実際のアクセスと同じ GET で確認するのが確実です。
URL=https://d1234abcdef.cloudfront.net/assets/main.abc123.js
for i in 1 2 3; do
curl -s -o /dev/null -D - "$URL" \
| grep -iE '^(x-cache|age|cache-control|x-amz-cf-pop):'
echo '---'
done
正常なら、2 回目から Hit になり Age(キャッシュされてからの秒数)が増えていきます。
cache-control: public, max-age=31536000, immutable
x-cache: Miss from cloudfront
x-amz-cf-pop: NRT57-P2
---
cache-control: public, max-age=31536000, immutable
x-cache: Hit from cloudfront
x-amz-cf-pop: NRT57-P2
age: 1
---
3 回とも Miss なら、次の早見表で原因を絞り込みます。x-amz-cf-pop(応答したエッジの識別子)が毎回違う場合は、エッジが変わっただけの可能性があります。
Miss が続く原因の早見表
| 確認結果 | 原因 | 対処 |
|---|---|---|
| 何回アクセスしても Miss(すべてのファイル) | ビヘイビアのキャッシュポリシーが CachingDisabled(最小・デフォルト・最大 TTL がすべて 0) |
対策1 |
cache-control が no-cache / no-store / private / max-age=0 |
オリジンがキャッシュ禁止を指示し、最小 TTL 0 のためそれに従っている | 対策2 |
URL の ?v=… や ?utm_… が変わると Miss |
クエリ文字列がキャッシュキーに入っている | 対策3 |
| Cookie 付き(ログイン後など)だと Miss | Cookie がキャッシュキーに入っている(旧設定の Forward Cookies: All など) | 対策3 |
| ブラウザや端末によって Miss | User-Agent などのヘッダーがキャッシュキーに入っている |
対策3 |
x-amz-cf-pop が変わった時だけ Miss |
キャッシュはエッジごとに別 | 正常(減らすなら対策4) |
| アクセスの少ないファイルだけ時々 Miss | 期限前でも、あまり要求されないオブジェクトは追い出される(evict) | 正常 |
| POST / PUT などのリクエスト | CloudFront がキャッシュするのは GET / HEAD(設定すれば OPTIONS)だけ | 仕様 |
| ブラウザの DevTools でだけ Miss が続く | ブラウザに保存された初回レスポンス(Miss)を見ている | curl で確認する |
対策1:キャッシュポリシーが CachingDisabled になっていないか確認する
S3 の静的ファイルなのに全部 Miss になる場合、まずビヘイビアに付いているキャッシュポリシーを確認します。
# 各ビヘイビアのキャッシュポリシー ID を確認
aws cloudfront get-distribution-config --id EXXXXXXXXXX \
--query 'DistributionConfig.{default:DefaultCacheBehavior.CachePolicyId,others:CacheBehaviors.Items[].{path:PathPattern,policy:CachePolicyId}}'
# ポリシーの TTL を確認
aws cloudfront get-cache-policy --id <ポリシーID> \
--query 'CachePolicy.CachePolicyConfig.{min:MinTTL,default:DefaultTTL,max:MaxTTL}'
よく使うマネージドポリシーの ID と TTL は次のとおりです(AWS 公式ドキュメント Use managed cache policies より)。
| ポリシー名 | ID | 最小 / デフォルト / 最大 TTL | キャッシュキー |
|---|---|---|---|
CachingDisabled |
4135ea2d-6df8-44a3-9df3-4b5a84be39ad |
0 / 0 / 0 秒 | なし(キャッシュしない) |
CachingOptimized |
658327ea-f89d-4fab-a63d-7e88639e58f6 |
1 秒 / 24 時間 / 365 日 | 正規化した Accept-Encoding のみ |
CachingDisabled なら、何度アクセスしても Miss になるのが正しい動作です。API やログイン後の画面用のビヘイビアに付けたポリシーが、デフォルトビヘイビア(*)にも付いていた、というのがよくある原因です。静的ファイルのパス(/assets/* など)には CachingOptimized を付けます。
CachePolicyId が null の場合はレガシーキャッシュ設定です。ビヘイビアの MinTTL / DefaultTTL / MaxTTL と ForwardedValues を確認してください。
対策2:S3 オブジェクトの Cache-Control を直す
最小 TTL が 0 のポリシーでは、オリジンが no-cache などを返すと CloudFront はキャッシュしません。
aws s3api head-object --bucket your-bucket-name --key assets/main.abc123.js \
--query '{CacheControl:CacheControl,ContentType:ContentType}'
既にアップロード済みのファイルの Cache-Control だけを書き換えるには、同じキーにコピーし直します。--metadata-directive REPLACE を付けるとメタデータが置き換わるので、Content-Type も一緒に指定し直してください。
aws s3 cp s3://your-bucket/assets/ s3://your-bucket/assets/ --recursive \
--metadata-directive REPLACE \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*" --include "*.js" --content-type "application/javascript"
ただし、index.html だけが Miss になるのは設計どおりです。後述の原因2のとおり、SPA の index.html は更新をすぐ反映させるために no-cache にするのが定石だからです。なお CachingOptimized は最小 TTL が 1 秒なので、no-cache の index.html でも 1 秒以内に続けてアクセスすると Hit になることがあります。
対策3:キャッシュキーからクエリ文字列・Cookie・ヘッダーを外す
キャッシュキーに入れた値は、値の組み合わせごとに別々にキャッシュされます。AWS 公式ドキュメント(Increase the proportion of requests that are served directly from the CloudFront caches)の例では、2 種類の Cookie にそれぞれ 3 通りの値があると、1 つのオブジェクトにつき最大 9 回オリジンへリクエストが飛びます。クエリ文字列も、parameter1=A と parameter1=a のような大文字小文字の違いや、パラメータの順番の違いだけで別のキャッシュになります。
- 静的ファイル(
/assets/*、*.cssなど)は、クエリ文字列・Cookie・ヘッダーをキャッシュキーに入れないビヘイビアに分ける - キャッシュキーに入れる場合は、オリジンが実際に出し分けに使うものだけを指定する
User-Agentのように値の種類が非常に多いヘッダーはキャッシュキーに入れない(端末で出し分けるならCloudFront-Is-Mobile-Viewerなどを使う)
広告の utm_source などが付いた URL でアクセスされるページは、クエリ文字列をすべてキャッシュキーに入れていると、パラメータの数だけ Miss になります。
対策4:エッジが変わるたびの Miss は Origin Shield で減らす
CloudFront のキャッシュはエッジロケーションごとに別々なので、別のエッジ(x-amz-cf-pop が違う)に初めて届いたリクエストは Miss になります。これは正常な動作です。オリジンへのリクエストを減らしたい場合は、Origin Shield を有効にすると、すべてのエッジとリージョナルエッジキャッシュが 1 か所のキャッシュ層を経由してオリジンに取りに行くようになります。この場合、ログの x-edge-detailed-result-type が OriginShieldHit になります。Origin Shield は追加料金がかかるので、オリジンの負荷が問題になっている時に検討してください。
原因1:CloudFront のデフォルト TTL が 24 時間のまま
もっともよくある原因です。CloudFront はデフォルトで 24 時間(86400 秒) オリジンのレスポンスをキャッシュします。Cache-Control ヘッダーを S3 に設定していない場合、このデフォルト値が使われます。
確認方法
# レスポンスヘッダーでキャッシュ状態を確認
curl -I https://d1234abcdef.cloudfront.net/index.html
レスポンスの X-Cache ヘッダーで状態がわかります。
X-Cache: Hit from cloudfront ← CloudFront キャッシュから返却
X-Cache: Miss from cloudfront ← S3(オリジン)から取得
Age ヘッダーはキャッシュされてからの経過秒数です。「古いファイルが出る」時は Hit で Age が大きいはずです。反対に Miss ばかりでキャッシュが効かない場合は、前の章「X-Cache: Miss from cloudfront とは?」の早見表を確認してください。
対処法:キャッシュポリシーの TTL を調整する
CloudFront コンソール → ディストリビューション → ビヘイビア → 編集 → キャッシュポリシー で設定します。
開発環境では TTL を短くし、本番でも静的アセット以外は短めにするのが基本です。
# AWS CLI でキャッシュポリシーを確認
aws cloudfront list-cache-policies --type custom
原因2:S3 オブジェクトに Cache-Control ヘッダーが設定されていない
S3 にアップロードしたファイルのメタデータに Cache-Control が設定されていないと、CloudFront は独自の判断(デフォルト TTL)でキャッシュします。
確認方法
# S3 オブジェクトのメタデータを確認
aws s3api head-object --bucket your-bucket-name --key index.html
出力に ContentType や CacheControl が含まれます。CacheControl が表示されていない場合は未設定です。
対処法:ファイルの種類に応じた Cache-Control を設定する
ファイルの役割によって最適な Cache-Control は異なります。
SPA の index.html(エントリーポイント)はキャッシュしない:
aws s3 cp dist/index.html s3://your-bucket/index.html \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html"
ハッシュ付き静的アセット(main.abc123.js など)は長期キャッシュ OK:
aws s3 sync dist/assets/ s3://your-bucket/assets/ \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*.html"
Vite や webpack はビルド時にファイル名にハッシュを付与するため、内容が変わればファイル名も変わります。そのため古いキャッシュが読まれることはなく、長期キャッシュが安全です。
GitHub Actions での自動デプロイ設定例:
- name: Deploy to S3
run: |
# index.html はキャッシュしない
aws s3 cp dist/index.html s3://${{ secrets.S3_BUCKET }}/index.html \
--cache-control "no-cache, no-store, must-revalidate" \
--content-type "text/html"
# ハッシュ付きアセットは長期キャッシュ
aws s3 sync dist/assets/ s3://${{ secrets.S3_BUCKET }}/assets/ \
--cache-control "public, max-age=31536000, immutable" \
--delete
AWS CDK での設定例:
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment'
// index.html:キャッシュなし
new s3deploy.BucketDeployment(this, 'DeployHtml', {
sources: [s3deploy.Source.asset('./dist', { exclude: ['assets/**'] })],
destinationBucket: bucket,
distribution,
distributionPaths: ['/*'],
cacheControl: [
s3deploy.CacheControl.fromString('no-cache, no-store, must-revalidate'),
],
})
// ハッシュ付きアセット:長期キャッシュ
new s3deploy.BucketDeployment(this, 'DeployAssets', {
sources: [s3deploy.Source.asset('./dist/assets')],
destinationBucket: bucket,
destinationKeyPrefix: 'assets',
cacheControl: [
s3deploy.CacheControl.fromString('public, max-age=31536000, immutable'),
],
})
原因3:CloudFront Invalidation の使い方が間違っている
キャッシュを手動でクリアするには Invalidation(キャッシュ無効化)を使いますが、落とし穴があります。
よくある間違い①:パスの先頭スラッシュを忘れる
# ❌ 間違い:スラッシュなし → 対象にヒットしない
aws cloudfront create-invalidation \
--distribution-id EXXXXXXXXXX \
--paths "index.html"
# ✅ 正しい:先頭にスラッシュが必須
aws cloudfront create-invalidation \
--distribution-id EXXXXXXXXXX \
--paths "/index.html"
# ✅ 全キャッシュをクリアする場合
aws cloudfront create-invalidation \
--distribution-id EXXXXXXXXXX \
--paths "/*"
よくある間違い②:完了を待たずにアクセス確認する
Invalidation は 完了まで 1〜5 分 かかります。コマンド実行直後にアクセスしても、まだ古いキャッシュが返ってくることがあります。
# Invalidation の状態を確認
aws cloudfront get-invalidation \
--distribution-id EXXXXXXXXXX \
--id IXXXXXXXXXXXXXXXXXXXXXXXXXX
# "Status": "Completed" になるまで待つ
# "Status": "InProgress" はまだ処理中
CI/CD での自動 Invalidation
デプロイのたびに Invalidation を自動実行するのがベストプラクティスです。
- name: Invalidate CloudFront cache
run: |
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*" \
--query 'Invalidation.Id' \
--output text)
echo "Waiting for invalidation $INVALIDATION_ID to complete..."
aws cloudfront wait invalidation-completed \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--id "$INVALIDATION_ID"
echo "Invalidation completed."
コスト注意: Invalidation は月 1,000 パス分まで無料、それ以降は有料です。
/*は1パスとしてカウントされるため、毎回全キャッシュをクリアしても費用はほぼかかりません。
原因4:ブラウザキャッシュと CloudFront キャッシュを混同している
「CloudFront のキャッシュはクリアした」と思っていても、実はブラウザキャッシュが効いているケースがあります。
確認方法
Chrome DevTools(F12)→ Network タブ → 「キャッシュを無効化」にチェックしてリロード。
または、シークレットモード(Ctrl+Shift+N)でアクセスしてみてください。シークレットモードではブラウザキャッシュが使われません。
レスポンスヘッダーでも判断できます。
# CloudFront がキャッシュを返している(HIT)
X-Cache: Hit from cloudfront
Age: 3600
# S3 から直接取得(MISS)
X-Cache: Miss from cloudfront
Age: 0
ブラウザキャッシュが原因の場合は、CloudFront の設定ではなく Cache-Control: no-cache ヘッダーで制御します。
原因5:S3 バケットポリシーと OAC の設定ミス
まれに、S3 のアクセス制御の問題で CloudFront が最新ファイルを取得できず、古いキャッシュを返し続けることがあります。
CloudFront の OAC(Origin Access Control)を使用している場合、S3 バケットポリシーが正しくないと CloudFront が 403 エラーを受け取ります。403 は、オリジンが Cache-Control: max-age(または s-maxage)を付けて返した場合にキャッシュされます(詳しくは原因6)。
S3 バケットポリシーの正しい設定(OAC 使用時)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EXXXXXXXXXX"
}
}
}
]
}
AWS:SourceArn にはディストリビューションの ARN を正確に指定してください(アカウント ID とディストリビューション ID は環境に合わせて変更)。
確認方法
# バケットポリシーを確認
aws s3api get-bucket-policy --bucket your-bucket-name
# CloudFront がオリジンから正常に取得できているか確認
# エラーページが返っていないかチェック
curl -v https://d1234abcdef.cloudfront.net/index.html 2>&1 | grep "< HTTP"
原因6:エラーレスポンスのキャッシュ(Error Caching TTL)
CloudFront は 4xx・5xx のエラーレスポンスもキャッシュします。キャッシュされる時間は「エラーキャッシュ最小 TTL(デフォルト 10 秒)」と「オリジンがエラーと一緒に返した Cache-Control: max-age / s-maxage」の長い方です(AWS 公式ドキュメント How CloudFront processes HTTP 4xx and 5xx status codes より)。
| ステータス | キャッシュされる条件 |
|---|---|
| 404, 414, 500, 501, 502, 503, 504 | 常にキャッシュされる |
| 400, 403, 405, 412, 415 | オリジンが max-age / s-maxage を付けた時だけ |
S3 の設定ミスで一度 404 などが返ったあと、設定を直しても最低 10 秒、エラーキャッシュ最小 TTL を長くしていればその間は古いエラーレスポンスが返り続けます。なお CachingDisabled ポリシーのビヘイビアでは、エラーもキャッシュされません。
確認・対処法
CloudFront コンソール → エラーページ → カスタムエラーレスポンスを設定し、エラーキャッシュ TTL を短くするか 0 にします。
# AWS CLI でエラーキャッシュの設定を確認(distribution config を取得)
aws cloudfront get-distribution-config --id EXXXXXXXXXX \
--query 'DistributionConfig.CustomErrorResponses'
まとめ:キャッシュが更新されない時のチェックリスト
| 確認項目 | 確認方法 |
|---|---|
| CloudFront キャッシュ状態 | curl -I URL で X-Cache を確認 |
| Miss が続く(Hit にならない) | キャッシュポリシーの TTL・オリジンの Cache-Control・キャッシュキー(クエリ/Cookie/ヘッダー) |
| S3 の Cache-Control ヘッダー | aws s3api head-object |
| Invalidation の完了 | コンソールまたは aws cloudfront get-invalidation |
| ブラウザキャッシュ | DevTools で「キャッシュを無効化」してリロード |
| S3 バケットポリシー | aws s3api get-bucket-policy |
| エラーレスポンスのキャッシュ | CloudFront コンソールのエラーページ設定 |
推奨 Cache-Control 設定まとめ
| ファイル種別 | Cache-Control | 理由 |
|---|---|---|
index.html |
no-cache, no-store, must-revalidate |
常に最新を取得させる |
| ハッシュ付き JS/CSS | public, max-age=31536000, immutable |
内容変化でファイル名も変わるため安全 |
| ハッシュなし JS/CSS | no-cache または短い TTL |
変更が反映されないリスクを避ける |
| 画像・フォント | public, max-age=86400 |
更新頻度に合わせて調整 |
CloudFront のキャッシュ問題は「どのレイヤーのキャッシュか」を特定することが解決への近道です。curl -I でヘッダーを確認し、X-Cache と Age を見れば CloudFront が原因かブラウザが原因か切り分けられます。上記のチェックリストを順に確認すれば、ほとんどのケースで原因を特定できます。