先に要点
- 新しく置いた画像が、配置したあとも 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 が長く残る流れ
最初は「配置に失敗した」と疑いたくなりますが、サーバーに直接アクセスすると 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 を開くのは、正しい習慣です。ただし、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 とはで整理しています。
参考にした情報
- RFC 9110 HTTP Semantics(15.1、15.5.5)
- RFC 9111 HTTP Caching(3、4.2.1、4.2.2、5.1)
- Caddy:header ディレクティブ、レスポンスマッチャー
- nginx:ngx_http_headers_module
- Cloudflare:ステータスコードごとのキャッシュ