Start Debugging

ASP.NET Core 11 における出力キャッシュとレスポンスキャッシュ:どちらを使うべきか

ASP.NET Core 11 では、ほぼすべてのサーバーサイドアプリにとって出力キャッシュが正しいデフォルトです。レスポンスキャッシュが勝るのは、HTTP ヘッダーを通じてブラウザーやプロキシのキャッシュを制御することが目的の場合だけです。ここでは機能マトリクスと、判断を左右する落とし穴とともに、その決定方法を示します。

ハンドラーを再実行せずにレスポンスを返したいと考えるほぼすべての ASP.NET Core 11 アプリにとって、答えは出力キャッシュ(AddOutputCache)です。これはサーバー制御であり、タグベースの無効化とキャッシュスタンピード対策をサポートし、判断をクライアントに委ねません。レスポンスキャッシュ(AddResponseCaching)に手を伸ばすのは、実際の目的が HTTP の Cache-ControlExpiresVary ヘッダーを設定して、ブラウザー、共有プロキシ、CDN が代わりにキャッシュするようにする、という狭いケースに限られます。自分のサーバーの負荷を減らそうとしているのなら、出力キャッシュが勝ります。この記事は Microsoft.NET.Sdk.Web と C# 14 を使った .NET 11(執筆時点で Preview 6、GA は 2026 年 11 月)を対象としていますが、出力キャッシュは ASP.NET Core 7 以降で安定しており、レスポンスキャッシュはさらに前から存在するため、このガイダンスは .NET 7 から 11 までそのまま当てはまります。

判断を決定づける唯一の違い

どちらの機能も、繰り返されるリクエストを安価なキャッシュヒットに変えられるため、人々はこれらを交換可能なものとして扱います。しかしそうではありません。両者の分かれ目は、誰がキャッシュを制御するかにあります。

レスポンスキャッシュは RFC 9111 の HTTP キャッシュを実装しています。これは HTTP キャッシュヘッダーの読み書きによって動作し、そして決定的なことに、クライアントのリクエストヘッダーを尊重します。Cache-Control: no-cache を送るクライアントは、あなたのサーバーに毎回レスポンスを再生成させ、サーバー側からそれに対してできることは何もありません。なぜなら、このミドルウェアは設計上、仕様に従うからです。これは HTTP キャッシュにとって正しい振る舞いです。HTTP キャッシュの目的は、クライアントとプロキシをまたいだネットワークレイテンシを減らすことであって、オリジンを負荷から守ることではありません。

ASP.NET Core 7 で追加された出力キャッシュは、これを逆転させます。何をどれくらいの間キャッシュするかをサーバーが決め、クライアントのヘッダーからは独立しています。悪意のあるクライアントや無知なクライアントが no-cache を送っても、あなたのキャッシュを破壊することはできません。この一点こそが、Microsoft 自身のドキュメントが現在サーバーアプリに出力キャッシュを推奨する理由であり、レスポンスキャッシュのドキュメントが UI アプリの読者を出力キャッシュへ誘導する理由です。「出力キャッシュ(.NET 7 以降で利用可能)は、UI アプリにとってより良いアプローチです。このシナリオでは、HTTP ヘッダーとは独立して構成が何をキャッシュするかを決定します。」

機能マトリクス

以下の各行は、.NET 11 と ASP.NET Core 11 のドキュメントに照らして検証済みです。

機能出力キャッシュレスポンスキャッシュ
導入時期ASP.NET Core 7ASP.NET Core 1.x
誰がキャッシュを制御するかサーバーHTTP ヘッダー(クライアントが上書き可能)
クライアントの Cache-Control: no-cache を尊重するかいいえ(サーバーが決定)はい(毎回再生成)
コピーの保管場所自分のサーバー上(インメモリまたは Redis)ブラウザー、プロキシ、CDN、およびそれ自身のミドルウェア
登録AddOutputCache() + UseOutputCache()AddResponseCaching() + UseResponseCaching()
エンドポイントごとのオプトイン.CacheOutput() / [OutputCache][ResponseCache] 属性 + ヘッダー
クエリによる VarySetVaryByQuery("key")VaryByQueryKeys(ミドルウェアが必要)
ヘッダーによる VarySetVaryByHeader("...")VaryByHeader -> Vary を出力
任意の値による VaryVaryByValue(...)サポートされない
タグベースの無効化あり、EvictByTagAsyncなし
キャッシュスタンピード対策あり、リソースロックがデフォルトで有効なし
分散ストアAddStackExchangeRedisOutputCache による Redis該当なし(インメモリのみ)
認証済みレスポンスをキャッシュするかデフォルトではいいえ(カスタムポリシーでオプトイン)いいえ(そしてすべきではない)
Set-Cookie のないレスポンスが必要かはい(Cookie はキャッシュを無効化する)はい
下流のキャッシュに指示するかいいえ(サーバーサイドのみ)はい、それこそが全目的

この表は形をはっきりさせます。出力キャッシュには、実際の API が必要とする運用面の機能(タグ、ロック、共有ストア)があります。レスポンスキャッシュには、出力キャッシュに欠けているものがちょうど一つあります。下流のキャッシュにあなたのレスポンスを保存させる HTTP ヘッダーを出力する、という点です。

違いを具体的にするために両方を配線する

出力キャッシュには 3 つの可動部分が必要で、インメモリの場合は NuGet パッケージが不要です。

// .NET 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOutputCache();

var app = builder.Build();

app.UseOutputCache();

app.MapGet("/catalog", GetCatalog)
    .CacheOutput(policy => policy.Expire(TimeSpan.FromMinutes(5)));

app.Run();

5 分以内に /catalog を 2 回叩くと、2 回目のリクエストでは GetCatalog は決して実行されません。レスポンスはサーバーメモリに保存され、そのまま返されます。クライアントのヘッダーは無関係です。

レスポンスキャッシュは表面的には似ていますが、振る舞いが異なります。

// .NET 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();
builder.Services.AddControllers();

var app = builder.Build();

app.UseResponseCaching();
app.MapControllers();

app.Run();
// .NET 11, C# 14 -- a controller action that sets caching headers
[ApiController]
[Route("api/[controller]")]
public sealed class CatalogController : ControllerBase
{
    [HttpGet]
    [ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any)]
    public IActionResult Get() => Ok(LoadCatalog());
}

この [ResponseCache] 属性は、レスポンスに Cache-Control: public,max-age=300 を書き込みます。ミドルウェアはコピーを保存するかもしれませんが、ブラウザーやあなたの前段にある CDN も同様に保存し、no-cache を送るクライアントはそれらすべてを飛ばします。ここでの成果物はヘッダーであって、ミドルウェアのインメモリコピーではありません。

出力キャッシュを選ぶべきとき

これはサーバーサイドアプリのデフォルトです。次の場合に選びます。

名前付きポリシー、MapGroup、Redis ストアを含む完全なエンドツーエンドのセットアップは、minimal API に出力キャッシュを追加する方法で解説しています。

レスポンスキャッシュを選ぶべきとき

レスポンスキャッシュは時代遅れではありません。気にかけているキャッシュが自分のものではない場合には、これが正しいツールです。

正直な位置づけに注意してください。これらのケースの大半では、レスポンスキャッシュのミドルウェアすら必要ありません。必要なのはヘッダーです。[ResponseCache] を追加する(あるいは自分で Cache-Control を書く)とヘッダーが設定されます。AddResponseCaching/UseResponseCaching はその上にサーバーサイドのミドルウェアコピーを加えるだけで、UI アプリにとってそのコピーはしばしば無用です。なぜなら、ブラウザーはそれを抑制するリクエストヘッダーを送るからです。したがって現実的な推奨はこうです。下流のキャッシュを制御するには HTTP キャッシュヘッダーを使い、サーバーサイドのコピーには出力キャッシュを使う。

「速い」を単なる印象論にしないための計測

どちらのキャッシュも、狙いはハンドラーを飛ばすことです。以下は、シミュレートされた 40 ms のハンドラーで、ヒットがミスに対してどれだけのコストになるかを示したものです。BenchmarkDotNet 0.15.x を使い、.NET 11(Preview 6)、Windows 11、Ryzen 9 7900X、インプロセスの TestServer で計測しました。

シナリオ中央値レイテンシハンドラーは実行された?
キャッシュなし(ベースライン、40 ms の処理)40.6 ms毎回
出力キャッシュ、ヒット0.11 msいいえ
レスポンスキャッシュ、ヒット(準拠したクライアント)0.12 msいいえ
レスポンスキャッシュ、クライアントが no-cache を送信40.5 msはい、毎回

2 つのキャッシュ技術は、クリーンなヒットでは見分けがつきません。どちらも 40 ms のハンドラーをおよそ 0.1 ms のミドルウェアに変えます。重要なのは最後の行です。行儀の悪い、あるいはプライバシーを気にする 1 つのクライアントが Cache-Control: no-cache を送るだけで、レスポンスキャッシュはフルコストに崩れ落ちますが、出力キャッシュは影響を受けません。なぜなら、クライアントではなくサーバーが判断を握っているからです。オリジンを守るためにキャッシュしているなら、その行こそが議論のすべてです。

あなたの代わりに決めてくれる落とし穴

好みに関係なく、3 つの要素が判断を強制します。

第一に、認証済みコンテンツです。どちらの機能もデフォルトでは認証済みレスポンスのキャッシュを拒否し、レスポンスキャッシュについてはドキュメントに明示的な警告があります。ユーザー識別によって内容が変わるコンテンツを決してキャッシュしてはならない、というものです。なぜなら Cache-Control: public は、あるユーザーのレスポンスを共有プロキシに漏らし、それが別のユーザーに提供されてしまうことがあるからです。出力キャッシュのデフォルトのガードレール(認証済みリクエストをキャッシュしない、Set-Cookie が存在するときはキャッシュしない)はより厳格で、サーバーによって強制されます。エンドポイントが認証の背後にあるなら、入念にテストされたカスタムポリシーを伴う出力キャッシュが唯一安全な道であり、それは上級者向けのケースとして扱うべきです。

第二に、無効化の要件です。「データが変わることがあり、古い読み取りは許容できない」が要件リストにあるなら、レスポンスキャッシュは対象外です。パージの仕組みを持たず、キャッシュされたレスポンスは max-age が切れるまで生き続けます。出力キャッシュの EvictByTagAsync こそ、あなたが実際に求めている機能です。

第三に、ストアがノードをまたいで生き残らなければならない場合です。タグベースの無効化を伴うロードバランサーの背後では、Redis の出力キャッシュストアが必要です。レスポンスキャッシュには分散の筋書きがありません。メソッドは AddStackExchangeRedisOutputCache であって、IDistributedCache に使われる似た名前の AddStackExchangeRedisCache ではないことに注意してください。また Microsoft は、素の IDistributedCache で出力キャッシュを裏付けることを推奨していません。そのインターフェースには、タグが依存するアトミックな操作が欠けているからです。

結論、再述

ASP.NET Core 11 では出力キャッシュをデフォルトにしましょう。これはサーバー制御であり、タグとスタンピード対策と本物の分散ストアを持ち、クライアントのヘッダーによって打ち負かされることがありません。レスポンスキャッシュを使う、より正確には [ResponseCache] を通じて HTTP キャッシュヘッダーを使うのは、埋めたいキャッシュが下流にある場合、つまり CDN、共有プロキシ、あるいはブラウザーにある場合だけです。両者は競合相手というより異なるレイヤーであり、一般的な本番構成では両方を使います。データベースを守るサーバーサイドのコピーには出力キャッシュを、エッジとブラウザーのコピー(ネットワークを守る)にはキャッシュヘッダーを使うのです。もし 1 つしか選べず、サーバー負荷を減らそうとしているなら、出力キャッシュを選びましょう。それはフレームワークが今あなたを誘導している方です。

関連記事

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る