サーバー ネットワーク ソフトウェア 公開日 2026.11.01 更新日 2026.11.01

CDN が 404 を長く覚えてしまうのはなぜ?Cache-Control を応答の状態で分ける設定

新しく置いた画像が、配置したあともしばらく 404 のまま。原因は、拡張子が画像なら長いキャッシュを付ける設定が、404 の応答にも付いていたことでした。404 は仕様上キャッシュされうる応答で、max-age が付くと CDN はその長さだけ覚えます。再現した結果、Caddy と nginx の設定の落とし穴、正常な応答にだけ付ける書き方、確かめ方、すでに残った 404 の消し方を整理しました。

目次(9項目)

先に要点

  • 新しく置いた画像が、配置したあとも 404 のまま消えないことがあります。原因は、「拡張子が画像なら長い Cache-Control を付ける」設定が、404 の応答にも付いていたことです。
  • 404 は仕様上、キャッシュされうる応答です。そこに max-age が付くと、CDN はその長さだけ「存在しない」と覚えます。
  • 配置の前に一度 URL を開く(確認の curl、クローラーのアクセス)だけで、その 404 が覚えられることがあります。正しい確認の習慣が、罠を踏ませます。
  • 直し方は、長いキャッシュを正常な応答(2xx と 304)にだけ付けることです。Caddy と nginx で、付く場合と付かない場合を実際に確かめました。
  • すでに残った 404 は、CDN のキャッシュ削除か、URL に版の印(?v=…)を付けて避けます。

Web サイトや画像を CDN の後ろで配信している方向けの記事です。「ファイルは確かにサーバーにあるのに、外からは 404 になる」ときに、サーバーではなくキャッシュを疑うための手がかりをまとめました。

この記事は、HTTP の仕様(RFC 9110・RFC 9111)、Caddy と nginx と Cloudflare の公式文書にもとづき、設定の挙動は Caddy 2.11.7 と nginx 1.28 を手元で動かして確かめています(2026年11月1日)。

何が起きるのか:配置前に開いた URL が、404 のまま残る

次の順で、起きます。

404 が長く残る流れ

  1. 1
    配置の前に URL を開く

    公開前の確認の curl や、クローラーのアクセスで、まだ無いファイルに届く。

  2. 2
    サーバーが 404 を返し、長い Cache-Control も付ける

    設定が「拡張子が画像なら max-age=604800(7日)」で、応答の状態を見ていない。

  3. 3
    CDN が 404 を保存する

    明示された max-age のとおり、7日間は「存在しない」と答える。

  4. 4
    ファイルを配置する

    サーバーには置けているが、CDN は保存した 404 を返し続ける。

  5. 5
    7日たつまで、404 のまま

    配置に失敗したように見えるが、サーバーには問題がない。

最初は「配置に失敗した」と疑いたくなりますが、サーバーに直接アクセスすると 200 で返ります。見るべきはCDN が返している応答の中身です。

なぜ 404 が保存されるのか:仕様上は普通のこと

HTTP の仕様は、404 をキャッシュされうる応答として定めています。

根拠 内容
RFC 9110 15.5.5 404 の応答は「ヒューリスティックにキャッシュ可能」。明示的な指示がなくても、キャッシュが再利用してよい
RFC 9110 15.1 ヒューリスティックにキャッシュ可能な状態コードとして、200・301・308・404・410 などを挙げている
RFC 9111 4.2.1 max-age があれば、それが鮮度の期間になる。応答が 404 でも同じ

つまり、404 に max-age=604800 が付いていれば、「7日間は新鮮」として扱うのが仕様どおりの動きです。CDN が間違っているのではなく、付けたのがサーバーの設定です。

付いていなくても、CDN は独自の既定で 404 を短く保存することがあります。たとえば Cloudflare は、origin が Cache-Control も Expires も付けない 404・410 を、既定で3分保存すると説明しています。製品ごとに違うので、お使いの CDN の文書で確かめてください。

「公開前に URL を確かめる」ことが、罠を踏ませる

配置の前に URL を開くのは、正しい習慣です。ただし、404 が保存される設定のままだと、確認そのものがキャッシュを汚します。確認は、配置のあとに行う、または別の URL(クエリ文字列を変えたもの)で行うと、この影響を受けません。

設定のどこが問題か:条件が「拡張子」だけ

問題の設定は、たとえば次のような形です。「応答の状態」を条件に入れていません。

# Caddy:画像の拡張子なら 7 日のキャッシュを付ける(応答の状態を見ていない)
@img path_regexp \.(png|ico|jpg)$
header @img Cache-Control "public, max-age=604800"
# nginx:always を付けると、404 を含むすべての応答に付く
location ~* \.(png|ico|jpg)$ {
    add_header Cache-Control "public, max-age=604800" always;
}

手元で、存在するファイル(a.png)と存在しないファイル(b.png)を請求して確かめた結果です。

設定 200(a.png) 404(b.png)
Caddy:上の形 public, max-age=604800 public, max-age=604800(404 にも付く)
nginx:add_header … always public, max-age=604800 public, max-age=604800(404 にも付く)
nginx:add_header(always なし) public, max-age=604800 付かない
nginx:expires 7d max-age=604800 と Expires 付かない

サーバーによって初期値が違うのが、わかりにくい点です。nginx の文書は、add_header も expires も、既定では 200・201・204・206・301・302・303・304・307・308 のときだけ付くと説明しています。always を付けた途端、404 にも付きます。「動いているからこのままでよい」と、別の設定から写したときに起きやすい形です。

確かめるときの注意

ブラウザの開発者ツールや curl -i のように、エラーの応答でもヘッダーをすべて表示する道具で確かめてください。道具によっては、404 のときにヘッダーを表示しないものがあります(手元の BusyBox の wget -S は、404 で Cache-Control を表示せず、付いていないように見えました)。

直し方:正常な応答にだけ付ける

長いキャッシュは、正常な応答(2xx と、変更なしを表す 304)にだけ付けます。

# Caddy:match で応答の状態を条件にする
@img path_regexp \.(png|ico|jpg)$
header @img Cache-Control "public, max-age=604800" {
    match status 2xx 304
}
# nginx:状態から値を決め、空なら付けない
map $status $cc {
    ~^(2|304) "public, max-age=604800";
    default   "";
}
location ~* \.(png|ico|jpg)$ {
    add_header Cache-Control $cc always;
}

手元で確かめた結果は次のとおりです。

設定 200 304 404
Caddy:match status 2xx 304 付く 付く 付かない
nginx:map と add_header $cc always 付く 付く 付かない

Caddy の header の match は、条件を満たす応答にだけヘッダー操作を適用するものです(公式文書)。304 を並べたのは、変更なしの応答にも同じ値を保つためです。手元の Caddy 2.11.7 で、条件つきの請求に対して 304 が返り、Cache-Control が付くことも確かめました。

付けなかった 404 には、CDN の既定が働きます。短く保存されるのが普通ですが、消したい 404 には、no-store のように明示するほうが確実です。

すでに 404 が残っているとき

設定を直しても、CDN にすでに保存された 404 は、期限が来るまで残ります。

CDN のキャッシュを消す

多くの CDN には、URL を指定してキャッシュを削除する機能があります。404 になっている URL だけを指定して消します。全体の削除は、他のファイルの負荷を増やすので避けます。

URL に版の印を付ける

参照する側を image.png?v=2 のように変えれば、別の URL として扱われ、保存済みの 404 を避けられます。SNS の画像のように、削除の手段を持てない側の回避として使えます。

待つ

どちらもできない場合は、max-age の期間(この例では7日)が過ぎるのを待ちます。残り時間は Age ヘッダーから見積もれます(RFC 9111 5.1)。

見分け方:サーバーと CDN を別々に見る

curl -i で次を比べると、どちらが原因かがわかります。

# CDN 経由の応答(状態、Cache-Control、Age、CDN の HIT/MISS を示すヘッダーを見る)
curl -i https://example.com/images/new.png

# origin に直接(CDN を通さない)。自分の環境の接続先に置き換える
curl -i --resolve example.com:443:<originのIPアドレス> https://example.com/images/new.png
見る点 判断
CDN 経由が 404、origin は 200 CDN が古い 404 を返している
404 に Cache-Control: max-age=… が付く origin の設定が、404 にも長いキャッシュを付けている
Age が増えていく、HIT を示すヘッダーが出る 保存された応答が返っている

CDN が HIT・MISS を示すヘッダーの名前は、製品ごとに違います。お使いの CDN の文書で確かめてください。

まとめ

条件に入れる

長いキャッシュは、拡張子だけでなく応答の状態(2xx と 304)も条件に入れます。

確かめる

設定を入れる前に、200 と 404 の両方を請求して、ヘッダーを見ます。

順序に注意

公開前の確認は、配置のあとに行うか、別のクエリ文字列で行います。

CDN の基本は CDN とは、Caddy の特徴は Caddy とはで整理しています。

参考にした情報

あとで見返すならここで保存

読み終わったあとに残しておきたい記事は、お気に入りからまとめて辿れます。