TL;DR
- コンピュータサイエンスの 2 大難問: cache invalidation、naming things、off-by-one errors (Phil Karlton)
- HTTP cache ヘッダをマスター:
Cache-Control、ETag、Vary、If-None-Match - CDN cache = 世界分散 cache: ユーザーに近い PoP から応答 → 50ms 以下の latency
- Stale-While-Revalidate (SWR): 古い応答を即座に返し、background で更新 = 体感上 100% の cache hit ratio
- Edge computing: cache を超えてコード実行 → Cloudflare Workers、Fastly Compute@Edge
1. なぜ cache は難しいのか
1.1 Phil Karlton の名言
"There are only two hard things in Computer Science: cache invalidation and naming things."
この冗談は半分本当です。cache invalidation は本当に難しい。
1.2 2 つの本質的な難しさ
1. いつ invalidate するか?
- 早すぎる → cache の効果がない
- 遅すぎる → stale data 問題
2. どう invalidate するか?
- 単一 key? パターン? 全体?
- 同期? 非同期?
- 失敗したらどうする?
1.3 cache 一貫性のトレードオフ
強い一貫性 ←─────────────────→ 高い性能
(リアルタイム) (cache hit)
すべてのシステムはこのスペクトル上のどこかに位置します。100% 両立はできません。
2. HTTP cache ヘッダをマスターする
2.1 Cache-Control — cache の中心
Cache-Control: public, max-age=3600, s-maxage=86400
ディレクティブ:
| 値 | 意味 |
|---|---|
public | CDN、ブラウザどちらも cache 可能 |
private | ブラウザのみ(ユーザーごと) |
no-cache | cache 可能、毎回検証が必要 |
no-store | 絶対に cache しない(機密データ) |
max-age=N | ブラウザ cache の有効時間(秒) |
s-maxage=N | CDN cache の有効時間(ブラウザより優先) |
must-revalidate | 期限切れ後は必ず検証 |
immutable | 絶対に変わらない(CSS、hash 付き JS) |
stale-while-revalidate=N | 期限切れ後 N 秒は stale を使用可能 |
stale-if-error=N | エラー時に N 秒間 stale を使用 |
2.2 一般的なパターン
HTML(頻繁に変化):
Cache-Control: public, max-age=0, must-revalidate
API JSON(変化しうる):
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400
静的ファイル(hash 含む):
Cache-Control: public, max-age=31536000, immutable
機密データ(ユーザー情報):
Cache-Control: private, no-store
2.3 ETag — 効率的な検証
サーバーがコンテンツの一意識別子(hash)を返します:
HTTP/1.1 200 OK
ETag: "abc123"
Cache-Control: max-age=3600
cache 期限切れ後、クライアントから:
GET /api/users/123 HTTP/1.1
If-None-Match: "abc123"
コンテンツが同一なら:
HTTP/1.1 304 Not Modified
ETag: "abc123"
304 Not Modified = body なし。帯域節約。
2.4 Last-Modified — 時刻ベース
HTTP/1.1 200 OK
Last-Modified: Tue, 15 Apr 2025 10:00:00 GMT
GET /api/users/123 HTTP/1.1
If-Modified-Since: Tue, 15 Apr 2025 10:00:00 GMT
ETag より弱い: 1 秒未満の変化を検知できない、時計同期に依存。
2.5 Vary — cache の同一性の定義
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Vary: Accept-Encoding, Accept-Language
意味: 「同じ URL でも Accept-Encoding(gzip vs br)や Accept-Language(en vs ko)が違えば別 cache」。
注意: Vary: User-Agent はほぼ無限のバリエーション → cache が無意味になります。絶対に使わないこと。
2.6 よくある誤解
1. no-cache は「cache しない」ではない
no-cache: cache 可、使う前に検証が必要no-store: cache 絶対 NG
2. Pragma: no-cache は古いヘッダ
- HTTP/1.0 の名残
- ほぼ無視されるが、互換性のため残す場所もある
3. Vary: * は cache 無効化
- すべてのヘッダが違えば別 cache = 決して hit しない
3. CDN の仕組み
3.1 CDN とは
Content Delivery Network — 世界中に分散した cache サーバーのネットワーク。
ユーザー(ソウル) → [CDN PoP ソウル] (cache hit!) → 応答
↓ miss
[Origin サーバー(米国)]
効果:
- ソウルのユーザー: 5ms (CDN PoP)
- 米国 origin に直接: 200ms(往復 + 処理)
3.2 CDN の構成要素
| コンポーネント | 役割 |
|---|---|
| PoP (Point of Presence) | 都市別の cache サーバー(Cloudflare は 300+) |
| Origin | 元サーバー(あなたのサーバー) |
| Edge Cache | PoP の cache ストレージ |
| DNS | 最も近い PoP にルーティング |
| Anycast | 同じ IP が複数 PoP にルーティング |
3.3 cache key
CDN は何で cache を識別するのか?
デフォルト: URL のみ。
GET /api/users/123 → cache
GET /api/users/123?lang=en → 別 cache(query string を含む)
Cloudflare Workers:
const cacheKey = new Request(url, { method: 'GET' })
const cache = caches.default
const cached = await cache.match(cacheKey)
3.4 cache hit ratio
最重要メトリクス。
hit_ratio = cache_hits / (cache_hits + cache_misses)
| Hit Ratio | 意味 |
|---|---|
| 95%+ | 優秀 |
| 90–95% | 良好 |
| 80–90% | 改善余地あり |
| 80% 未満 | cache 戦略を見直す必要あり |
1% の改善に大きな意味: 96% → 97% = origin トラフィック 25% 減。
3.5 CDN 比較
| Cloudflare | Fastly | CloudFront | Akamai | Vercel/Netlify | |
|---|---|---|---|---|---|
| PoP 数 | 300+ | 80+ | 600+ | 4000+ | 100+ |
| Edge compute | Workers (V8) | Compute@Edge (Wasm) | Lambda@Edge | EdgeWorkers | Edge Functions |
| 無料枠 | 寛大 | なし | なし | なし | あり |
| 価格 | 非常に安い | 高い | 普通 | 高い | 普通 |
| cache invalidation | 即時 | 150ms (Instant Purge) | 分単位 | 分単位 | 分単位 |
| DDoS 対策 | 優秀 | 良好 | 良好 | 優秀 | 普通 |
Cloudflare: コスパチャンピオン。90% のユースケースに適合。 Fastly: 高速な invalidation、メディア/ニュース向けに強い。 CloudFront: AWS 連携が強み。 Akamai: エンタープライズ、最多の PoP。
4. Cache Invalidation — 最も難しい部分
4.1 4 つの invalidation 戦略
1. TTL ベース: 時間経過で自動期限切れ
Cache-Control: max-age=3600 // 1 時間後に期限切れ
長所: シンプル。短所: 1 時間は stale の可能性。
2. Push(即時 invalidation): コンテンツ変更時に CDN へ通知
curl -X POST https://api.cloudflare.com/zones/.../purge_cache \
-H "Authorization: Bearer ..." \
-d '{"files":["https://example.com/api/users/123"]}'
長所: 即時。短所: 複雑、コスト。
3. ETag ベース: リクエストごとに検証(304 応答)
- 帯域は節約、遅延は残る
4. Versioned URL: 変更時に新 URL
/static/main.abc123.js → /static/main.def456.js
長所: invalidation 自体が不要。CSS/JS の標準。
4.2 Cloudflare の invalidation API
# 単一 URL
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {api_token}" \
-d '{"files":["https://example.com/page1"]}'
# タグベース(Enterprise)
curl -X POST ... -d '{"tags":["user-123"]}'
# 全体
curl -X POST ... -d '{"purge_everything":true}'
4.3 Cache Tags — 優雅な invalidation
Fastly と Cloudflare Enterprise が対応:
HTTP/1.1 200 OK
Cache-Tag: user-123, post-456, blog-list
invalidation:
curl -X POST https://api.fastly.com/service/{id}/purge/user-123
→ user-123 タグの付いたすべての cache を即座に無効化。
利用例: ユーザーがプロフィールを更新 → user-123 タグ付きの全ページを無効化。
4.4 invalidation の難しいシナリオ
シナリオ 1: ユーザーがコメントを投稿
影響を受けるページ:
- /post/123 (該当の記事)
- /post/123/comments
- /user/abc/comments (投稿者ページ)
- /sidebar/recent-comments
- ... さらに多数
→ 全ページを一つずつ invalidate? タグベースが答え。
シナリオ 2: データベース一貫性
DB トランザクション commit → cache invalidation
↓ もし invalidation が失敗したら?
stale data!
→ リトライ + dead letter queue。または eventual consistency を受け入れる。
シナリオ 3: invalidation の急増
ブラックフライデー = 価格変更が頻発 = invalidation 急増 → CDN API rate limit。
→ batch invalidation、タグ活用、TTL 短縮。
5. Stale-While-Revalidate — ゲームチェンジャー
5.1 問題
従来の cache:
[cache 期限切れ] → [origin リクエスト] → [待機] → [応答]
↑ ユーザーが待つ(遅い!)
5.2 Stale-While-Revalidate
Cache-Control: max-age=60, stale-while-revalidate=86400
意味:
- 60 秒間: fresh な cache を使用
- 60 秒~86460 秒: stale cache を即座に返す + background で更新
- 86460 秒以降: 期限切れ(origin へ)
5.3 ユーザー体験
ユーザー 1 (60s): [cache hit, fresh] instant
ユーザー 2 (61s): [cache hit, stale] instant (background 更新開始)
ユーザー 3 (62s): [cache hit, fresh] instant (更新直後)
全員 instant 応答! origin はたまにしか呼ばれない。
5.4 Next.js の ISR (Incremental Static Regeneration)
export async function getStaticProps() {
const data = await fetchData()
return {
props: { data },
revalidate: 60 // 60 秒後に background で再生成
}
}
内部的に SWR パターンを利用。静的並みの速度 + 動的データ。
5.5 stale-if-error
Cache-Control: max-age=60, stale-if-error=86400
意味: origin エラー時に 24 時間 stale データを返す。
効果: origin ダウン時もサイトが生きる。障害耐性の要。
6. Cache パターン — コードレベル
6.1 Cache-Aside (Lazy Loading)
def get_user(user_id):
user = cache.get(f"user:{user_id}")
if user is None:
user = db.query(f"SELECT * FROM users WHERE id={user_id}")
cache.set(f"user:{user_id}", user, ttl=3600)
return user
最も一般的。シンプルで安定。欠点: cache miss 時の初回は遅い。
6.2 Read-Through
cache が origin を直接呼ぶ(アプリは cache のみを呼ぶ)。
# cache ライブラリが自動処理
user = cache.get(f"user:{user_id}", loader=lambda: db.query(...))
長所: コードがシンプル。短所: cache ライブラリ依存。
6.3 Write-Through
書き込み時に cache も同時更新。
def update_user(user_id, data):
db.execute(f"UPDATE users SET ... WHERE id={user_id}")
cache.set(f"user:{user_id}", data, ttl=3600)
長所: cache が常に最新。短所: 書き込みが遅く、使われないデータも cache。
6.4 Write-Behind (Write-Back)
書き込みは cache のみ → 非同期で DB 反映。
def update_user(user_id, data):
cache.set(f"user:{user_id}", data, ttl=3600)
queue.push({"action": "update", "id": user_id, "data": data})
長所: 非常に高速な書き込み。短所: cache 障害時にデータ損失リスク。
6.5 Cache Stampede の防止
問題: 人気 key が期限切れになると数千リクエストが同時に origin を叩く。
解決策:
1. Mutex(分散ロック)
def get_user(user_id):
user = cache.get(f"user:{user_id}")
if user is None:
with cache.lock(f"lock:user:{user_id}", timeout=10):
user = cache.get(f"user:{user_id}") # double-check
if user is None:
user = db.query(...)
cache.set(f"user:{user_id}", user, ttl=3600)
return user
2. Probabilistic Early Expiration TTL 直前から一部リクエストを早めに更新:
def get_with_recompute(key):
value, ttl = cache.get_with_ttl(key)
if random() < ttl_factor(ttl):
# 一部リクエストだけ前倒しで再計算
recompute_async(key)
return value
3. Stale-While-Revalidate(前述)
7. Edge Computing — cache を超えて
7.1 Edge でのコード実行
CDN は単なる cache ではありません。コードも実行:
- Cloudflare Workers (V8 isolate)
- Fastly Compute@Edge (WebAssembly)
- AWS Lambda@Edge (Node.js, Python)
- Vercel Edge Functions (V8)
- Deno Deploy (V8)
7.2 ユースケース
1. A/B テスト
export default {
async fetch(request) {
const variant = Math.random() < 0.5 ? 'a' : 'b'
return fetch(`https://origin.com/page-${variant}`)
}
}
2. geo ルーティング
const country = request.cf.country
const origin = country === 'KR' ? 'asia.api.com' : 'us.api.com'
return fetch(origin + new URL(request.url).pathname)
3. 認証/認可
const token = request.headers.get('Authorization')
const user = await verifyJWT(token)
if (!user) return new Response('Unauthorized', { status: 401 })
return fetch(origin)
4. HTML 変換
const response = await fetch(origin)
return new HTMLRewriter()
.on('h1', { element(el) { el.setInnerContent('Modified!') } })
.transform(response)
5. API 集約
const [user, posts] = await Promise.all([
fetch('https://api.com/user/123').then(r => r.json()),
fetch('https://api.com/posts?user=123').then(r => r.json())
])
return new Response(JSON.stringify({ user, posts }))
7.3 Edge computing の制約
- CPU 時間制限: 通常 10–50ms (Cloudflare Workers は 50ms)
- メモリ制限: 128MB (Cloudflare)、50MB (Fastly)
- コールドスタート: V8 isolate はほぼ皆無(5ms 以下)
- DB アクセス: edge から origin DB は遅い → edge DB が必要(Turso、D1、Neon)
7.4 Edge データストア
| Cloudflare | Fastly | Vercel | |
|---|---|---|---|
| KV | Workers KV | Object Store | KV |
| DB | D1 (SQLite) | - | Neon、Turso |
| Object | R2 | - | Blob |
| Cache | Cache API | Cache | - |
| Vector | Vectorize | - | - |
8. CDN cache のベストプラクティス
8.1 静的アセット
# CSS、JS (hash 付きファイル名)
Cache-Control: public, max-age=31536000, immutable
# 画像(あまり変わらない)
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
8.2 HTML
# 頻繁に変わるが cache 可能
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
max-age=0: ブラウザは毎回検証s-maxage=300: CDN は 5 分 cachestale-while-revalidate=86400: 24 時間 stale 使用可
8.3 API JSON
# ユーザー別データ (private)
Cache-Control: private, max-age=60, must-revalidate
# 公開 API
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400
8.4 ユーザー別コンテンツ
# 絶対に cache しない
Cache-Control: private, no-store
あるいは cache key に Cookie を含める(危険 — カーディナリティ爆発)。
8.5 圧縮 + cache
HTTP/1.1 200 OK
Content-Encoding: gzip
Cache-Control: public, max-age=3600
Vary: Accept-Encoding
Vary: Accept-Encoding は必須 — gzip vs br vs none は別 cache。
9. モニタリングとデバッグ
9.1 レスポンスヘッダの確認
curl -I https://example.com/api/users/123
HTTP/2 200
cf-cache-status: HIT
age: 234
cache-control: public, max-age=3600
主要ヘッダ:
cf-cache-status: HIT、MISS、EXPIRED、BYPASS、REVALIDATEDage: cache されてからの秒数x-cache: AWS CloudFront ヘッダ
9.2 cache 状態
HIT ← cache から返却
MISS ← origin リクエスト
EXPIRED ← 期限切れ、更新中
REVALIDATED ← 304 で検証済み
BYPASS ← cache 迂回 (no-cache 等)
DYNAMIC ← cache 不可
9.3 cache hit ratio の追跡
多くの CDN ではダッシュボードに表示:
- Cloudflare Analytics
- Fastly Insights
- CloudFront Reports
9.4 デバッグ手順
cache hit しない場合、以下を確認:
- Cache-Control ヘッダ:
no-cache、privateがあるか? - Set-Cookie: cookie があると cache されない
- Vary: バリエーションが多すぎる
- Method: POST はほぼ cache されない
- Status code: 200、301、302、404、410 のみ cache
- Query string: CDN が query string を無視する設定?
10. 実戦 — ブログサイトの cache
10.1 シナリオ
- WordPress または Next.js ブログ
- 1 日 100 万ページビュー
- 記事が頻繁に更新される
- コメントシステム
10.2 cache 戦略
HTML ページ:
Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400
静的アセット:
Cache-Control: public, max-age=31536000, immutable
API JSON:
Cache-Control: public, max-age=300, stale-while-revalidate=86400
10.3 invalidation
記事更新時:
- タグ invalidation:
post-id、category-slug、homepage - 新 URL: 静的アセットは hash 変更
// Next.js + Cloudflare
async function updatePost(id, data) {
await db.update(...)
await cf.purgeByTags([`post-${id}`, 'homepage'])
}
10.4 結果
- Cache hit ratio: 95%+
- Origin requests: 5 万/日 (95% 削減)
- 応答時間: 50ms (CDN) vs 500ms (origin)
- コスト: 95% 削減
クイズ
1. no-cache と no-store の違いは?
答え: no-cache: cache 可能、ただし使う前に必ず origin へ検証リクエスト(If-None-Match)。304 を受け取れば cache を使える。意外にも cache が活用される。no-store: cache 絶対不可。リクエストと応答をどこにも保存しない(メモリ、ディスク)。機密データに使用。よくある間違い: 「cache しない」のつもりで no-cache を使う → 実際には cache されて検証されるだけ。
2. Stale-While-Revalidate がゲームチェンジャーである理由は?
答え: ユーザーに常に instant 応答を返せます。cache 期限切れ後も stale データを即座に返し、background で更新。ユーザーは待たず、次回リクエストから新データが見える。結果: 体感 cache hit ratio が 100%、origin トラフィックも少ないまま。Next.js ISR はこのパターンを使用。SWR は cache 期限切れの本質的なトレードオフ(新鮮さ vs 速度)をほぼ解消します。
3. Cache Stampede を防ぐ方法を 3 つ挙げよ
答え: (1) Mutex / 分散ロック — 期限切れの key に対して 1 リクエストだけ origin を呼び、他は結果を待つ(または stale を利用)、(2) Probabilistic Early Expiration — TTL 直前から一部リクエストを前倒しで再計算、(3) Stale-While-Revalidate — 期限切れ cache も使いつつ background 更新。人気 key ほどこの保護が重要。Reddit や Facebook も類似パターンを使用。
4. CDN cache hit ratio が 1% 改善するとどうなる?
答え: origin トラフィックが 25% 以上減ります。95% → 96% なら origin リクエストは 5% → 4% = 20% 削減。96% → 97% なら 4% → 3% = 25% 削減。cache hit ratio が高いほど 1% の価値が大きくなります。これは (1) サーバーコスト、(2) 帯域コスト、(3) 応答時間、(4) 信頼性すべてに影響。0.5% の改善でも大きなビジネス価値。
5. Edge computing が cache を超えた意味とは?
答え: CDN はもはや静的ファイルの cache だけではありません。ユーザーに近いノードでコードを実行します。ユースケース: A/B テスト、geo ルーティング、認証、HTML 変換、API 集約。結果: 動的コンテンツも 50ms 以下。Cloudflare Workers は V8 isolate でコールドスタートが 5ms 以下。Fastly Compute@Edge は Wasm ベース。origin サーバーの役割は縮小し、edge がますます多くの仕事を担う。
参考資料
- MDN HTTP Caching
- RFC 7234 — HTTP Caching
- RFC 5861 — Stale Content — SWR の定義
- Cloudflare Cache Docs
- Fastly Caching Best Practices
- HTTP Caching Best Practices — Google
- Cache Stampede
- SWR React Library — Vercel
- Cloudflare Workers
- Fastly Compute@Edge
- Vercel Edge Functions
- HTTP Caching Decision Tree — Harry Roberts
현재 단락 (1/353)
- **コンピュータサイエンスの 2 大難問**: cache invalidation、naming things、off-by-one errors (Phil Karlton)