Redis 使い方:Next.js で Cache-Tag と再検証を実装(Edge/Node 両対応)
Next.js アプリケーションでキャッシュ戦略を構築する際、Redis は強力な選択肢となります。特に、Cache-Tag(キャッシュタグ)を活用することで、関連するキャッシュを一括で無効化できるため、データの整合性を保ちながら高速なレスポンスを実現できるでしょう。
本記事では、Redis を使った Cache-Tag の実装方法と再検証(Revalidation)の仕組みを、Edge Runtime と Node.js Runtime の両方に対応する形で解説します。初心者の方でも実装できるよう、段階的にコード例を示していきますね。
背景
Next.js 13 以降では App Router が導入され、サーバーコンポーネントやデータフェッチングの仕組みが大きく変わりました。
従来の Pages Router では getServerSideProps や getStaticProps でキャッシュを制御していましたが、App Router では fetch API の拡張や revalidateTag、revalidatePath といった新しい機能が提供されています。これにより、よりきめ細かなキャッシュ管理が可能になりました。
しかし、Next.js の標準キャッシュだけでは、複数のページやコンポーネントにまたがるデータの整合性を保つのが難しい場合があります。例えば、ブログ記事を更新した際に、記事詳細ページだけでなく、記事一覧ページやカテゴリページのキャッシュも同時に無効化したいケースです。
そこで Redis を導入し、Cache-Tag の仕組みを実装することで、関連するキャッシュを効率的に管理できるようになります。
下図は、Next.js アプリケーションと Redis を組み合わせたキャッシュ管理の基本構造を示しています。
mermaidflowchart TB
user["ユーザー"] -->|リクエスト| nextjs["Next.js App Router"]
nextjs -->|キャッシュ確認| redis[("Redis<br/>Cache Store")]
redis -->|キャッシュヒット| nextjs
redis -->|キャッシュミス| api["Backend API / DB"]
api -->|データ取得| redis
redis -->|タグ付きで保存| redis
nextjs -->|レスポンス| user
admin["管理者"] -->|更新操作| webhook["Webhook / API"]
webhook -->|タグで再検証| redis
redis -->|該当キャッシュ削除| redis
図で理解できる要点
- Redis がキャッシュストアとして機能し、Next.js からのリクエストを高速化
- タグを使って関連するキャッシュをグループ化
- 更新時にタグ指定で一括無効化が可能
課題
Next.js で Redis を活用したキャッシュ管理を実装する際、以下のような課題に直面します。
Edge Runtime と Node.js Runtime の両対応が必要
Next.js では、同じアプリケーション内で Edge Runtime と Node.js Runtime が混在するケースがあります。Edge Runtime は起動が高速で CDN エッジで実行できる一方、利用できる Node.js API に制限があるため、Redis クライアントライブラリの選択が重要になります。
従来の redis パッケージは Node.js 専用で、Edge Runtime では動作しません。そのため、@upstash/redis のような HTTP ベースの Redis クライアントを使う必要がありますね。
Cache-Tag の管理と紐付け
Redis 自体には Cache-Tag の概念がありません。そのため、タグとキャッシュキーの紐付けを自分で実装する必要があります。
具体的には、タグごとにキーのセット(Set 型)を保持し、キャッシュ保存時にタグとキーを関連付け、再検証時にはタグから関連キーを取得して一括削除する仕組みが必要です。
再検証タイミングの制御
Next.js の revalidateTag や revalidatePath は便利ですが、これらは Next.js の内部キャッシュのみを対象としています。Redis のキャッシュと Next.js の内部キャッシュを同期させるには、API Route や Server Actions から明示的に Redis のキャッシュを削除する処理を実装しなければなりません。
以下の図は、Cache-Tag 管理の課題を示したものです。
mermaidflowchart LR
cache1["キャッシュ<br/>posts:1"] -->|タグ| tag1["tag:posts"]
cache2["キャッシュ<br/>posts:2"] -->|タグ| tag1
cache3["キャッシュ<br/>posts:list"] -->|タグ| tag1
cache3 -->|タグ| tag2["tag:list"]
tag1 -.->|再検証時に<br/>一括削除| cache1
tag1 -.->|一括削除| cache2
tag1 -.->|一括削除| cache3
style tag1 fill:#ffcccc
style tag2 fill:#ccccff
図で理解できる要点
- 複数のキャッシュに同じタグを付与することでグループ化
- 再検証時にタグを指定すると、関連する全キャッシュが削除される
- 1 つのキャッシュに複数タグを設定することも可能
解決策
上記の課題を解決するため、以下のアプローチで実装を進めます。
Upstash Redis を採用
Upstash Redis は HTTP ベースの Redis サービスで、Edge Runtime と Node.js Runtime の両方で動作します。専用の SDK @upstash/redis を使うことで、環境を問わず同じコードでキャッシュ操作が可能になりますね。
また、Serverless 環境に最適化されており、接続プールの管理が不要なため、Next.js との相性も抜群です。
Cache-Tag 管理の実装パターン
Cache-Tag の仕組みを実装するには、以下の 3 つの操作が必要です。
| # | 操作 | 説明 |
|---|---|---|
| 1 | キャッシュ保存 | データとタグを Redis に保存し、タグごとにキーを記録 |
| 2 | キャッシュ取得 | キーからデータを取得 |
| 3 | タグで再検証 | タグに紐づく全キーを取得し、一括削除 |
この仕組みをユーティリティ関数として実装することで、アプリケーション全体で再利用できるようになります。
Next.js の revalidateTag と連携
Next.js の revalidateTag は、Server Actions や Route Handlers から呼び出せます。この関数を呼び出すタイミングで、同時に Redis のキャッシュも削除することで、両方のキャッシュを同期できますね。
以下の図は、再検証フローの全体像を示しています。
mermaidsequenceDiagram
participant Admin as 管理者
participant API as API Route
participant NextCache as Next.js キャッシュ
participant RedisCache as Redis キャッシュ
participant DB as データベース
Admin->>API: データ更新リクエスト
API->>DB: データ更新
DB-->>API: 更新完了
API->>NextCache: revalidateTag("posts")
NextCache-->>API: Next.js キャッシュ削除
API->>RedisCache: Redis タグで削除
RedisCache-->>API: Redis キャッシュ削除
API-->>Admin: 更新成功レスポンス
Note over NextCache,RedisCache: 両方のキャッシュが<br/>同期して削除される
図で理解できる要点
- データ更新時に Next.js と Redis の両方のキャッシュを削除
revalidateTagで Next.js 内部キャッシュを無効化- 同時に Redis のタグ指定削除を実行して同期を保つ
具体例
それでは、実際のコードを段階的に見ていきましょう。
プロジェクトセットアップ
まず、必要なパッケージをインストールします。
bashyarn add @upstash/redis
yarn add -D @types/node
Upstash Redis のアカウントを作成し、以下の環境変数を .env.local に設定してください。
envUPSTASH_REDIS_REST_URL=https://your-redis-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here
これで、Redis への接続準備が整いました。
Redis クライアントの初期化
Redis クライアントを初期化するファイルを作成します。
typescript// lib/redis.ts
import { Redis } from '@upstash/redis';
// Redis クライアントのシングルトンインスタンスを作成
// 環境変数から接続情報を取得します
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
このファイルでは、Upstash Redis クライアントをインスタンス化しています。環境変数から URL とトークンを読み込むことで、セキュアに接続情報を管理できますね。
Cache-Tag ユーティリティの型定義
次に、Cache-Tag を管理するユーティリティ関数の型を定義します。
typescript// lib/cache-tags.ts
// キャッシュオプションの型定義
export interface CacheOptions {
// キャッシュの有効期限(秒単位)
ttl?: number;
// キャッシュに付与するタグの配列
tags?: string[];
}
// キャッシュ取得結果の型定義
export interface CacheResult<T> {
// キャッシュされたデータ(存在しない場合は null)
data: T | null;
// キャッシュがヒットしたかどうか
hit: boolean;
}
型定義により、TypeScript の型チェックが効くようになり、開発時のミスを防げます。
キャッシュ保存関数の実装
タグ付きでキャッシュを保存する関数を実装します。
typescript// lib/cache-tags.ts(続き)
import { redis } from './redis';
/**
* タグ付きでデータをキャッシュに保存します
* @param key キャッシュキー
* @param data 保存するデータ
* @param options キャッシュオプション(TTL、タグ)
*/
export async function setCache<T>(
key: string,
data: T,
options: CacheOptions = {}
): Promise<void> {
const { ttl = 3600, tags = [] } = options;
// データをキャッシュに保存(TTL 付き)
if (ttl > 0) {
await redis.set(key, JSON.stringify(data), { ex: ttl });
} else {
await redis.set(key, JSON.stringify(data));
}
// タグが指定されている場合、タグとキーの紐付けを保存
if (tags.length > 0) {
await Promise.all(
tags.map((tag) => {
const tagKey = `tag:${tag}`;
// タグキーに対して、キーを Set に追加
return redis.sadd(tagKey, key);
})
);
}
}
この関数では、データを JSON 文字列に変換して Redis に保存します。同時に、指定されたタグごとに Set 型のキーを作成し、キャッシュキーを追加していますね。
TTL(Time To Live)を設定することで、一定時間後に自動的にキャッシュが削除される仕組みです。
キャッシュ取得関数の実装
保存したキャッシュを取得する関数を実装します。
typescript// lib/cache-tags.ts(続き)
/**
* キャッシュからデータを取得します
* @param key キャッシュキー
* @returns キャッシュ結果(データとヒット情報)
*/
export async function getCache<T>(
key: string
): Promise<CacheResult<T>> {
// Redis からデータを取得
const cached = await redis.get(key);
// キャッシュが存在しない場合
if (cached === null) {
return { data: null, hit: false };
}
// JSON をパースしてデータを返す
try {
const data = JSON.parse(cached as string) as T;
return { data, hit: true };
} catch (error) {
// JSON パースエラーの場合はキャッシュミスとして扱う
console.error('Cache parse error:', error);
return { data: null, hit: false };
}
}
この関数は、指定されたキーでキャッシュを取得し、JSON パースしてオブジェクトとして返します。キャッシュがヒットしたかどうかを hit プロパティで判定できるため、デバッグやモニタリングに活用できますね。
タグによる再検証関数の実装
最も重要な、タグによるキャッシュ一括削除の関数を実装します。
typescript// lib/cache-tags.ts(続き)
/**
* 指定したタグに紐づく全キャッシュを削除します
* @param tag 削除対象のタグ
* @returns 削除されたキーの数
*/
export async function revalidateCacheByTag(
tag: string
): Promise<number> {
const tagKey = `tag:${tag}`;
// タグに紐づくキーの一覧を取得
const keys = await redis.smembers(tagKey);
// キーが存在しない場合は何もしない
if (!keys || keys.length === 0) {
return 0;
}
// 全キーを削除
await redis.del(...keys);
// タグキー自体も削除
await redis.del(tagKey);
return keys.length;
}
この関数は、指定されたタグに紐づく全キーを取得し、一括で削除します。タグキー自体も削除することで、次回のキャッシュ保存時に新しいタグセットが作成されますね。
Server Component でのキャッシュ活用
実際に Server Component でキャッシュを活用する例を見ていきます。
typescript// app/posts/page.tsx
import { getCache, setCache } from '@/lib/cache-tags';
// 記事データの型定義
interface Post {
id: number;
title: string;
content: string;
createdAt: string;
}
// API から記事一覧を取得する関数
async function fetchPosts(): Promise<Post[]> {
const res = await fetch('https://api.example.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function PostsPage() {
const cacheKey = 'posts:list';
// キャッシュから取得を試みる
const cached = await getCache<Post[]>(cacheKey);
let posts: Post[];
if (cached.hit) {
// キャッシュヒット
posts = cached.data!;
} else {
// キャッシュミス:API から取得
posts = await fetchPosts();
// キャッシュに保存(TTL: 1時間、タグ: posts, list)
await setCache(cacheKey, posts, {
ttl: 3600,
tags: ['posts', 'list'],
});
}
return (
<div>
<h1>記事一覧</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
この例では、記事一覧をキャッシュから取得し、キャッシュミスの場合のみ API を呼び出しています。キャッシュ保存時に posts と list の 2 つのタグを付与することで、柔軟な再検証が可能になりますね。
動的ページでのキャッシュ活用
記事詳細ページでも同様にキャッシュを活用できます。
typescript// app/posts/[id]/page.tsx
import { getCache, setCache } from '@/lib/cache-tags';
interface Post {
id: number;
title: string;
content: string;
createdAt: string;
}
// 記事詳細を取得する関数
async function fetchPost(id: string): Promise<Post> {
const res = await fetch(
`https://api.example.com/posts/${id}`
);
if (!res.ok) throw new Error('Failed to fetch post');
return res.json();
}
export default async function PostDetailPage({
params,
}: {
params: { id: string };
}) {
const cacheKey = `posts:${params.id}`;
// キャッシュから取得
const cached = await getCache<Post>(cacheKey);
let post: Post;
if (cached.hit) {
post = cached.data!;
} else {
// API から取得してキャッシュに保存
post = await fetchPost(params.id);
await setCache(cacheKey, post, {
ttl: 3600,
tags: ['posts', `post:${params.id}`],
});
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<time>{post.createdAt}</time>
</article>
);
}
ここでは、記事 ID ごとにキャッシュキーを作成し、個別のタグ post:${id} を付与しています。これにより、特定の記事だけを再検証することも、全記事を一括で再検証することも可能になりますね。
API Route での再検証実装
記事を更新する際の API Route を実装します。
typescript// app/api/posts/[id]/route.ts
import { revalidateCacheByTag } from '@/lib/cache-tags';
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
// 記事更新の型定義
interface UpdatePostRequest {
title?: string;
content?: string;
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body: UpdatePostRequest = await request.json();
// 外部 API で記事を更新
const res = await fetch(
`https://api.example.com/posts/${params.id}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}
);
if (!res.ok) {
throw new Error('Failed to update post');
}
const updatedPost = await res.json();
// Next.js の内部キャッシュを再検証
revalidateTag('posts');
revalidateTag(`post:${params.id}`);
// Redis のキャッシュも再検証
await revalidateCacheByTag('posts');
await revalidateCacheByTag(`post:${params.id}`);
return NextResponse.json({
success: true,
data: updatedPost,
});
} catch (error) {
console.error('Post update error:', error);
return NextResponse.json(
{ success: false, error: 'Failed to update post' },
{ status: 500 }
);
}
}
この API Route では、記事更新後に Next.js の revalidateTag と Redis の revalidateCacheByTag の両方を呼び出しています。これにより、Next.js の内部キャッシュと Redis のキャッシュが同期され、常に最新のデータが表示されますね。
Server Actions での再検証
Server Actions を使った再検証の例も見ていきましょう。
typescript// app/actions/posts.ts
'use server';
import { revalidateCacheByTag } from '@/lib/cache-tags';
import { revalidateTag } from 'next/cache';
// 記事削除の Server Action
export async function deletePost(postId: string) {
try {
// 外部 API で記事を削除
const res = await fetch(
`https://api.example.com/posts/${postId}`,
{
method: 'DELETE',
}
);
if (!res.ok) {
throw new Error('Failed to delete post');
}
// Next.js キャッシュを再検証
revalidateTag('posts');
revalidateTag('list');
revalidateTag(`post:${postId}`);
// Redis キャッシュを再検証
await revalidateCacheByTag('posts');
await revalidateCacheByTag('list');
await revalidateCacheByTag(`post:${postId}`);
return { success: true };
} catch (error) {
console.error('Delete post error:', error);
return {
success: false,
error: 'Failed to delete post',
};
}
}
Server Actions からも同様に、Next.js と Redis の両方のキャッシュを再検証できます。Server Actions は 'use server' ディレクティブを付けることで、クライアントコンポーネントから直接呼び出せるため、フォーム送信などで活用できますね。
Edge Runtime 対応の確認
最後に、Edge Runtime でも動作することを確認します。
typescript// app/api/cache-info/route.ts
import { redis } from '@/lib/redis';
import { NextResponse } from 'next/server';
// Edge Runtime を指定
export const runtime = 'edge';
export async function GET() {
try {
// Redis からキャッシュキーの一覧を取得
const keys = await redis.keys('posts:*');
return NextResponse.json({
runtime: 'edge',
cacheKeys: keys,
count: keys.length,
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to get cache info' },
{ status: 500 }
);
}
}
このエンドポイントは Edge Runtime で動作し、Upstash Redis を使ってキャッシュ情報を取得します。export const runtime = 'edge' を指定するだけで、同じコードが Edge 環境で実行されますね。
下表は、実装したキャッシュ管理システムの主要機能をまとめたものです。
| # | 機能 | 説明 | 使用場面 |
|---|---|---|---|
| 1 | setCache | データをタグ付きで保存 | Server Component でのデータ取得後 |
| 2 | getCache | キャッシュからデータ取得 | Server Component の最初の処理 |
| 3 | revalidateCacheByTag | タグでキャッシュ一括削除 | API Route / Server Actions でのデータ更新後 |
| 4 | Edge/Node 両対応 | Upstash Redis で環境非依存 | 全ての Runtime で利用可能 |
まとめ
本記事では、Next.js で Redis を活用した Cache-Tag と再検証の実装方法を解説しました。
Upstash Redis を採用することで、Edge Runtime と Node.js Runtime の両方で動作するキャッシュシステムを構築できます。Cache-Tag の仕組みを実装することで、関連するキャッシュを効率的に管理し、データの整合性を保ちながら高速なレスポンスを実現できるでしょう。
重要なポイントは以下の通りです。
- Upstash Redis を使うことで、Edge/Node 両環境で同じコードが動作
- Cache-Tag をタグキーと Set 型で実装し、関連キャッシュをグループ化
- 再検証時 は Next.js の
revalidateTagと Redis のrevalidateCacheByTagを同時実行 - Server Component でキャッシュを活用し、API 呼び出しを最小化
- Server Actions / API Route でデータ更新後に両方のキャッシュを削除
この実装パターンを応用すれば、EC サイトの商品キャッシュ、ニュースサイトの記事キャッシュ、SaaS のダッシュボードデータなど、さまざまなユースケースに対応できますね。
ぜひ、あなたのプロジェクトでも Redis を活用したキャッシュ戦略を試してみてください。
関連リンク
articleRedis 使い方:Next.js で Cache-Tag と再検証を実装(Edge/Node 両対応)
articleRedis キャッシュ設計大全:Cache-Aside/Write-Through/Write-Behind の実装指針
articleRedis コマンド 100 本ノック:Key/Hash/List/Set/ZSet/Stream 早見表
articleRedis セットアップ完全版(macOS/Homebrew):設定テンプレ付き最短構築
articleRedis vs Memcached 徹底比較:スループット・メモリ効率・機能差の実測
articleRedis OOM を根絶:maxmemory・eviction・大キー検出の実践トリアージ
articleRedis 使い方:Next.js で Cache-Tag と再検証を実装(Edge/Node 両対応)
articleNext.js の RSC 境界設計:Client Components を最小化する責務分離戦略
articleNext.js ルーティング早見表:セグメント・グループ・オプションの一枚まとめ
articleNext.js × pnpm/Turborepo 初期構築:ワークスペース・共有パッケージ・CI 最適化
articleNext.js Route Handlers vs API Routes:設計意図・性能・制約のリアル比較
articleRemix と Next.js/Vite/徹底比較:選ぶべきポイントはここだ!
articleESLint 運用ダッシュボード:SARIF/Code Scanning で違反推移を可視化
articleSolidJS 本番運用チェックリスト:CSP・SRI・Preload・エラーレポートの総点検
articleRedis 使い方:Next.js で Cache-Tag と再検証を実装(Edge/Node 両対応)
articleDify 本番運用ガイド:SLO/SLA 設定とアラート設計のベストプラクティス
articleCursor の KPI 設計:リードタイム・欠陥率・レビュー時間を定量で追う
articlePython 本番運用 SLO 設計:p95 レイテンシ・エラー率・スループットの指標化
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来