Claude Codeで実アプリ向けキャッシュ戦略を設計する方法
HTTP、CDN、Service Worker、Redisを実アプリで使い分けるClaude Code向けキャッシュ設計ガイド。
キャッシュは「速くする魔法」ではありません。実アプリでは、HTTPヘッダー、CDN、Service Worker、Redis、プロセス内メモリがそれぞれ別の場所で別の寿命を持ちます。ここを曖昧にしたままClaude Codeへ「キャッシュを入れて」と頼むと、表示は速くなっても、古い価格、消えたはずの在庫、別ユーザーの情報漏えいにつながります。
この記事では、Claude Codeに実装を任せる前に決めるべきキャッシュ戦略を、初心者でも追える順番で整理します。Masaが小さなExpressアプリと記事サイトの検証環境で試したときも、効果が大きかったのは複雑なテクニックではなく「どの層に、何を、何秒置くか」を先に決めたことでした。
全体像は次のように考えると迷いにくくなります。
flowchart LR
User[Browser] --> Http[HTTP cache]
Http --> SW[Service worker Cache API]
SW --> CDN[CDN or edge cache]
CDN --> App[Node or app server]
App --> Redis[Redis or app cache]
App --> DB[(Database)]
まず決めること
キャッシュ設計は、ツール名からではなくデータの性質から始めます。Claude Codeには、次の4点を明示してからコードを書かせるのが安全です。
- 誰に同じ内容を見せてよいか
- 何秒古くても業務上許されるか
- 更新時にどのキーやURLを消すか
- 事故時に誰が、どの順番で戻すか
たとえば、ロゴ画像やハッシュ付きJavaScriptは1年キャッシュしても問題ありません。一方、ログイン後の請求情報は共有キャッシュに置いてはいけません。用語でいうと、ブラウザ専用の保存場所はプライベートキャッシュ、CDNのように複数ユーザーで共有される保存場所は共有キャッシュです。MDNのHTTP cachingでも、この違いが重要な前提として説明されています。
キャッシュ層の比較表
| 層 | 向いているデータ | 目安TTL | 無効化の考え方 | よくある失敗 |
|---|---|---|---|---|
| HTTPブラウザキャッシュ | 画像、CSS、JS、公開APIの短い応答 | 1分から1年 | ファイル名変更、ETag、Cache-Control | APIまで長期保存して古い画面になる |
| CDN、エッジ | 商品一覧、記事HTML、OGP画像 | 30秒から1日 | URL単位、タグ単位、デプロイ時purge | ログイン後HTMLを共有してしまう |
| Service Worker Cache API | オフラインページ、静的シェル、低頻度更新のJSON | バージョン単位 | キャッシュ名のバージョン更新 | 古いService Workerが残り続ける |
| Redis、アプリキャッシュ | DB集計、外部API結果、ランキング | 10秒から1時間 | キー設計、更新イベント、TTL | KEYSで本番Redisを詰まらせる |
| プロセス内メモリ | 設定値、feature flagの短期コピー | 数秒から数分 | 再起動、明示clear | 複数台構成で値がずれる |
この表をCLAUDE.mdに入れておくと、Claude Codeが新しいAPIや画面を追加するときも判断がぶれにくくなります。CLAUDE.mdの書き方はClaude CodeのCLAUDE.mdベストプラクティスも参考になります。
実例1: ExpressでHTTPキャッシュを付ける
最初に入れるべきなのは、RedisではなくHTTPヘッダーです。Cache-Controlは、ブラウザやCDNに「何をどれくらい保存してよいか」を伝える標準ヘッダーです。詳細なディレクティブはMDNのCache-Control headerで確認できます。
次のコードはそのままserver.jsとして動かせます。
npm install express
node server.js
// server.js
const express = require("express");
const app = express();
function cacheControl(req, res, next) {
const path = req.path;
if (path.startsWith("/assets/")) {
res.set("Cache-Control", "public, max-age=31536000, immutable");
return next();
}
if (path.startsWith("/api/private/")) {
res.set("Cache-Control", "no-store");
return next();
}
if (path.startsWith("/api/public/")) {
res.set("Cache-Control", "public, max-age=60, s-maxage=300, stale-while-revalidate=600");
res.set("Vary", "Accept-Encoding");
return next();
}
res.set("Cache-Control", "no-cache");
next();
}
app.use(cacheControl);
app.use("/assets", express.static("public/assets"));
app.get("/api/public/products", (_req, res) => {
res.json({ items: ["book", "template", "consultation"], generatedAt: new Date().toISOString() });
});
app.get("/api/private/me", (_req, res) => {
res.json({ userId: "demo-user", plan: "team" });
});
app.listen(3000, () => {
console.log("http://localhost:3000");
});
ポイントは、公開データと個人データをURLで分けることです。/api/private/にno-storeを付けておけば、ブラウザもCDNも保存しません。公開APIはmax-ageを短めにし、CDNにはs-maxageで少し長めに置けます。
Claude Codeには「ログイン後のレスポンスには必ずno-storeを付ける」「公開APIだけs-maxageを許可する」と指示すると、レビュー時の見落としが減ります。
実例2: RedisのgetOrSetヘルパー
RedisはDBや外部APIの負荷を下げるのに向いています。ただし、キー名とTTLを決めずに導入すると、どこで古くなるのか追えなくなります。ここではキャッシュアサイド、つまり「先にRedisを見る、なければ本体から取得して保存する」形にします。
npm install redis
// cache.js
const { createClient } = require("redis");
const redis = createClient({
url: process.env.REDIS_URL || "redis://localhost:6379",
});
let connecting;
async function client() {
if (redis.isOpen) return redis;
if (!connecting) connecting = redis.connect();
await connecting;
return redis;
}
async function getOrSet(key, ttlSeconds, loader) {
const r = await client();
const cached = await r.get(key);
if (cached !== null) {
return JSON.parse(cached);
}
const fresh = await loader();
await r.set(key, JSON.stringify(fresh), { EX: ttlSeconds });
return fresh;
}
async function invalidate(keys) {
const r = await client();
if (keys.length > 0) {
await r.del(keys);
}
}
module.exports = { getOrSet, invalidate };
利用側は次のように書けます。
// products.js
const { getOrSet, invalidate } = require("./cache");
async function loadProductsFromDb() {
return [
{ id: "p1", name: "Prompt Templates", price: 500 },
{ id: "p2", name: "Claude Code Consultation", price: 15000 },
];
}
async function getPublicProducts() {
return getOrSet("products:list:v1", 300, loadProductsFromDb);
}
async function updateProduct(productId, patch) {
console.log("update db", productId, patch);
await invalidate(["products:list:v1", `products:item:${productId}:v1`]);
}
module.exports = { getPublicProducts, updateProduct };
本番ではKEYS products:*のような全件探索は避けます。キー数が増えるとRedisを止める原因になります。必要なら、更新対象を配列で持つ、Redis Setに関連キーを登録する、またはSCANを使う運用に変えてください。
実例3: Service Worker Cache APIのバージョン管理
Service Workerは、ブラウザ内でリクエストを横取りできる仕組みです。組み合わせて使うCache APIは、オフライン画面や静的シェルを保存するのに便利です。ただし、ここは事故も多い層です。古いService Workerが残ると、デプロイ後も古いJSやCSSを配り続けます。
// public/sw.js
const CACHE_VERSION = "claude-code-cache-v2026-06-01";
const STATIC_CACHE = `${CACHE_VERSION}:static`;
const PRECACHE_URLS = ["/", "/offline.html", "/assets/app.css"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(STATIC_CACHE)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((names) =>
Promise.all(
names
.filter((name) => !name.startsWith(CACHE_VERSION))
.map((name) => caches.delete(name))
)
)
.then(() => self.clients.claim())
);
});
self.addEventListener("fetch", (event) => {
const request = event.request;
if (request.method !== "GET") return;
event.respondWith(
caches.match(request).then((cached) => {
if (cached) return cached;
return fetch(request)
.then((response) => {
if (response.ok && new URL(request.url).pathname.startsWith("/assets/")) {
const copy = response.clone();
caches.open(STATIC_CACHE).then((cache) => cache.put(request, copy));
}
return response;
})
.catch(() => caches.match("/offline.html"));
})
);
});
登録コードはアプリのクライアント側エントリに置きます。
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js");
});
}
更新時はCACHE_VERSIONを必ず変えます。Claude Codeへ依頼するときも「Service Workerのキャッシュ名は日付またはビルドIDでバージョン管理する」と明記してください。
実例4: Claude Codeにキャッシュ監査を頼むプロンプト
Claude Codeの強みは、実装そのものよりも「どこにキャッシュを置くべきか」をリポジトリ全体から確認できる点です。Claude Code common workflowsにあるように、調査、編集、テストを小さな作業単位で進めると安全です。
そのまま使える監査プロンプトは次の通りです。
あなたはWebアプリのキャッシュ監査担当です。
このリポジトリでHTTPヘッダー、CDN前提、Service Worker、Redis、プロセス内メモリを使っている箇所を調べてください。
出力してください。
1. キャッシュ層ごとの対象データ
2. TTLと無効化条件
3. 個人情報や認証済みレスポンスが共有キャッシュに乗るリスク
4. stale dataが起きる具体的な画面
5. 最小修正案と、修正後に実行する確認コマンド
制約:
- 推測した箇所は推測と明記する
- 既存の設計に合わせる
- 大きなリファクタリングは提案だけに止める
このプロンプトを使うと、Claude Codeは「Redisを追加しましょう」だけでなく、既存のCache-Control、Service Worker、APIルート、DB更新処理を横断して見てくれます。プロンプトも一種のコンテキストキャッシュです。毎回同じ説明を打つのではなく、監査観点をテンプレート化して、必要な差分だけ渡すと精度が安定します。
実アプリでのユースケース
1つ目はECや教材販売の公開商品一覧です。商品名、価格、サムネイルは全員に同じ内容を見せられるため、CDNに短く置き、DB集計はRedisに5分置く構成が向いています。更新ボタンを押したら商品一覧キーとCDNの対象URLを消します。
2つ目は管理画面のダッシュボードです。売上集計やPV集計は毎秒正確でなくてもよいのでRedisに30秒から5分置けます。ただし、ユーザー権限や個人別の通知はprivateまたはno-storeにします。
3つ目はドキュメントやブログです。記事HTMLはCDNで短めに、画像やハッシュ付きアセットは長期に、オフライン用の殻はService Workerで管理します。ClaudeCodeLabのように記事数が多いサイトでは、公開直後だけpurgeし、通常アクセスはCDNで受けると体感が安定します。
4つ目は外部API連携です。為替、天気、SaaSのプラン情報などはAPI制限に引っかかりやすいので、Redisで短く吸収します。ただし、規約上キャッシュが禁止されているAPIもあるため、Claude Codeには公式ドキュメント確認をタスクに含めてください。
失敗例と落とし穴
一番危険なのは、ログイン済みHTMLをCDNに保存することです。Cookieを見て内容が変わるページは、privateまたはno-storeを基本にします。どうしても共有キャッシュに置くなら、ユーザー固有部分をAPIで後から取得する構成に分けます。
次に多いのは、Varyの不足です。言語、圧縮、認証状態でレスポンスが変わるのにVaryがないと、別条件の応答が再利用されます。多言語サイトではVary: Accept-Languageが必要か、URLで言語を完全に分けるかを決めます。
Redisでは、キャッシュスタンピードに注意します。これは人気キーが同時に期限切れになり、多数のリクエストが一斉にDBへ流れる現象です。対策はTTLにランダムな揺らぎを足す、ロックを使う、stale-while-revalidate的に古い値を短く返す、の3つです。
Service Workerでは、削除済みファイルを古いキャッシュから返す事故が起きます。デプロイごとにバージョンを変え、activateで旧キャッシュを削除し、緊急時はService Workerを無効化する手順も用意してください。
無効化ランブック
キャッシュは「入れる」より「消す」ほうが難しいです。最低限、次のランブックを運用メモに置きます。
- 影響範囲を決める。商品、記事、ユーザー、全体のどれかを明記する。
- DB更新を先に完了し、失敗したらキャッシュを消さない。
- Redisキーを削除する。大量削除は
SCANや関連キーリストを使う。 - CDNをURL単位またはタグ単位でpurgeする。全purgeは最終手段にする。
- Service Workerのバージョンを上げ、旧キャッシュ削除を確認する。
curl -I、ブラウザDevTools、Redisのヒット率で確認する。- 事故時はTTLを短くするか
no-storeへ寄せ、後で段階的に戻す。
この手順をClaude Codeへ渡すときは、「ランブックを満たさない実装は完了扱いにしない」と明記すると効果的です。チーム運用ならClaude Code生産性を3倍にするTipsで紹介しているように、完了条件と確認コマンドをセットにしてください。
ClaudeCodeLabの教材と相談
自分のプロジェクトに合わせたキャッシュ方針まで作るなら、ClaudeCodeLabの教材・テンプレート一覧でCLAUDE.mdやレビュー用プロンプトの型を先に整えると早いです。チーム導入、CDN、Redis、権限境界、レビュー運用まで一緒に設計したい場合は、Claude Code導入相談・研修で実リポジトリ前提の整理ができます。
公式情報を確認するなら、HTTPの基本はMDNのHTTP caching、Cache APIはCache API reference、ヘッダー詳細はCache-Control、Claude Codeの作業フローはCommon workflowsを見るのが安全です。
この記事で紹介した内容を実際に試した結果、Masaの検証アプリでは静的アセットの再取得が減り、公開APIはCDN向けTTLで安定し、RedisのgetOrSetでDBログの無駄な読み込みが見えやすくなりました。一方で、Service Workerはバージョン削除を入れないと古いCSSが残りました。つまり、速さより先に「どこで古くなるか」を書くことが、Claude Codeでキャッシュを安全に入れる一番の近道です。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code Permission Receipt Pattern: 許可、証拠、ロールバックを残す運用
Claude Codeの権限運用を安全にする permission receipt。許可範囲、承認待ち、検証コマンド、CTA導線を記録します。
Claude CodeとCodex、結局どっち?事故らない“併用”の現実解
OpenAIのCodexとClaude Code、どっちが得意でどっちに任せる?両方を安全に併用する作業分担と権限・検証のワークフローを、僕の失敗談つきで解説します。
Claude Codeサブエージェント実装ガイド: 記事・コード作業を安全に並列委譲する方法
Claude Codeサブエージェントで記事・コード作業を安全に並列化する実装ガイド。委譲基準、プロンプト、失敗例を解説。