ETag と条件付きリクエスト(304 Not Modified)

入門 | 11分 で読める | 2026.05.02

公式ドキュメント

この記事の要点

ETagはリソースの「指紋」で、内容が変わったかを効率的に検証
If-None-Matchヘッダーで条件付きリクエストを送り、変更がなければ304 Not Modifiedで転送量を削減
• CDNやブラウザキャッシュと組み合わせて、パフォーマンスを最大化

ETagとは

ETag(Entity Tag)は、HTTPレスポンスヘッダーで送信される、リソースの特定バージョンを識別する文字列です。ファイルの内容から生成されるハッシュ値や、更新タイムスタンプを使って、「このリソースが変更されたかどうか」を効率的に判定できます。

なぜ必要か: ファイル全体を毎回送信すると帯域を無駄に消費します。ETags を使えば、「内容が変わっていない」ことを確認し、304レスポンスでボディを省略できます。

基本的なフロー

1. 初回リクエスト

クライアントがリソースを初めて取得します。

GET /style.css HTTP/1.1
Host: example.com

レスポンス:

HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 1234
ETag: "abc123xyz"
Cache-Control: max-age=3600

/* CSS content */

ブラウザはETag: "abc123xyz"を記録します。

2. 条件付きリクエスト(キャッシュ期限切れ後)

max-age=3600が経過すると、ブラウザは再検証します。

GET /style.css HTTP/1.1
Host: example.com
If-None-Match: "abc123xyz"

3a. 内容が変わっていない場合

HTTP/1.1 304 Not Modified
ETag: "abc123xyz"
Cache-Control: max-age=3600

ポイント: ボディがないため、転送量が大幅に削減されます。ブラウザはキャッシュから再利用します。

3b. 内容が変わった場合

HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 1456
ETag: "def456uvw"
Cache-Control: max-age=3600

/* Updated CSS content */

ポイント: 304レスポンスはボディを含まないため、1MBの画像でもヘッダーだけ(数百バイト)で応答できます。

ETagの生成方法

ファイルハッシュ

ファイル内容のMD5やSHA-256ハッシュを使用します。

const crypto = require('crypto');
const fs = require('fs');

function generateETag(filePath) {
  const content = fs.readFileSync(filePath);
  const hash = crypto.createHash('md5').update(content).digest('hex');
  return `"${hash}"`;
}

// ETag: "5d41402abc4b2a76b9719d911017c592"

利点:

  • 内容が1バイトでも変われば異なるETags
  • 複数サーバーで同じファイルなら同じETags

欠点:

  • ファイルサイズが大きいとハッシュ計算に時間がかかる

ファイルメタデータ

更新時刻とサイズの組み合わせを使用します。

function generateETag(filePath) {
  const stats = fs.statSync(filePath);
  const mtime = stats.mtime.getTime();
  const size = stats.size;
  return `"${mtime}-${size}"`;
}

// ETag: "1714521600000-1234"

利点:

  • 高速(ハッシュ計算不要)

欠点:

  • タイムスタンプが異なるサーバーで一致しない可能性

Nginxのデフォルト実装

Nginxは「最終更新時刻のタイムスタンプ(16進数)-ファイルサイズ(16進数)」を使用します。

ETag: "6626f2a0-4d2"
  • 6626f2a0: 最終更新時刻(Unixタイムスタンプの16進数)
  • 4d2: ファイルサイズ(1234バイトの16進数)

実践メモ: 複数サーバーでロードバランシングする場合、同じ内容には同じETagを返すことが重要です。Nginxのデフォルトは時刻ベースなので注意が必要です。

強いETag と 弱いETag

強いETag(Strong ETag)

ETag: "abc123xyz"

意味: バイト単位で完全一致。1バイトでも違えば異なるETag。

用途: 画像、動画、実行ファイルなど、正確性が重要なリソース。

弱いETag(Weak ETag)

ETag: W/"abc123xyz"

意味: 意味的に同等。多少の違いは許容。

用途:

  • 動的HTML(広告部分が変わる程度)
  • gzip圧縮(圧縮レベルの違いを無視)
  • テキストファイル(改行コードの違いを無視)

例(Nginx設定):

location /api/ {
  # gzip圧縮時に弱いETagを使用
  gzip on;
  etag on;
}

圧縮前と圧縮後で内容が異なるため、弱いETagを使います。

注意: If-Matchヘッダー(PUT/PATCHでの楽観的ロック)では、弱いETagは使用できません。強いETagのみ有効です。

条件付きリクエストヘッダー一覧

ヘッダー用途レスポンス
If-None-MatchGETでキャッシュ検証304 Not Modified
If-MatchPUT/PATCH/DELETEで楽観的ロック412 Precondition Failed
If-Modified-SinceGETで時刻ベース検証304 Not Modified
If-Unmodified-SincePUT/PATCH/DELETEで時刻ベースロック412 Precondition Failed
If-Range部分ダウンロード継続の検証206 Partial Content / 200 OK

If-None-Match(キャッシュ検証)

GET /image.jpg HTTP/1.1
If-None-Match: "abc123xyz"

サーバーの判定:

  • ETagが一致 → 304 Not Modified
  • ETagが不一致 → 200 OK(新しい内容を返す)

If-Match(楽観的ロック)

PUT /api/posts/123 HTTP/1.1
If-Match: "abc123xyz"
Content-Type: application/json

{"title": "Updated Title"}

サーバーの判定:

  • ETagが一致 → 更新実行、200 OK
  • ETagが不一致 → 412 Precondition Failed(誰かが先に更新済み)

ユースケース: 複数ユーザーが同じリソースを編集する際、古いデータで上書きするのを防ぐ。

// フロントエンド
const response = await fetch('/api/posts/123');
const data = await response.json();
const etag = response.headers.get('ETag');

// ユーザーが編集

// 保存時にETagを送信
await fetch('/api/posts/123', {
  method: 'PUT',
  headers: {
    'If-Match': etag,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ title: 'Updated Title' })
});

Last-Modified と If-Modified-Since

ETagの代わりに、タイムスタンプベースの検証も可能です。

初回レスポンス

HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT
Content-Length: 1234

条件付きリクエスト

GET /style.css HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT

レスポンス

HTTP/1.1 304 Not Modified
Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT

ETag vs Last-Modified

項目ETagLast-Modified
精度内容ベース(正確)時刻ベース(1秒単位)
複数サーバー対応ハッシュなら一致時刻ずれで不一致
計算コスト高(ハッシュ計算)低(ファイルメタデータ)
優先度高(両方あればETag優先)

ポイント: ETagとLast-Modifiedの両方がある場合、ブラウザはETagを優先します。両方使うことで、古いクライアントにも対応できます。

実装例

Express.js(Node.js)

ExpressはデフォルトでETags を自動生成します。

const express = require('express');
const app = express();

// デフォルトでETags有効
app.get('/api/posts', (req, res) => {
  const posts = getPosts();
  res.json(posts);
  // ETagが自動で付与される
});

// カスタムETag
app.get('/image.jpg', (req, res) => {
  const filePath = './image.jpg';
  const etag = generateETag(filePath);
  
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }
  
  res.set('ETag', etag);
  res.sendFile(filePath);
});

// ETags無効化
app.set('etag', false);

Nginx

http {
  # ETag有効(デフォルト)
  etag on;
  
  # 静的ファイル配信
  location /static/ {
    root /var/www;
    etag on;
    
    # Cache-Controlと併用
    add_header Cache-Control "public, max-age=3600";
  }
  
  # API レスポンス
  location /api/ {
    proxy_pass http://backend;
    
    # バックエンドのETagを保持
    proxy_set_header If-None-Match $http_if_none_match;
  }
}

Apache


  # ETag有効(デフォルト)
  FileETag MTime Size
  
  # またはハッシュベース
  FileETag Digest


# キャッシュコントロールと併用

  Header set Cache-Control "public, max-age=3600"

Cloudflare Workers

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const cache = caches.default;
    
    // キャッシュチェック
    let response = await cache.match(request);
    
    if (!response) {
      // オリジンから取得
      response = await fetch(request);
      
      // ETag付きでキャッシュ
      const headers = new Headers(response.headers);
      const etag = generateETag(await response.clone().arrayBuffer());
      headers.set('ETag', etag);
      
      const newResponse = new Response(response.body, { headers });
      await cache.put(request, newResponse.clone());
      return newResponse;
    }
    
    // If-None-Match チェック
    const ifNoneMatch = request.headers.get('If-None-Match');
    const etag = response.headers.get('ETag');
    
    if (ifNoneMatch === etag) {
      return new Response(null, {
        status: 304,
        headers: { 'ETag': etag }
      });
    }
    
    return response;
  }
};

CDNとの連携

Vary: Accept-Encoding との組み合わせ

圧縮形式ごとに異なるETagが必要です。

HTTP/1.1 200 OK
Content-Encoding: br
ETag: "abc123-br"
Vary: Accept-Encoding

理由: gzip圧縮版とBrotli圧縮版で内容が異なるため、異なるETags が必要。

CDNのETag書き換え

一部のCDNは独自のETagを付与します。

# オリジン
ETag: "abc123"

# CDN経由
ETag: "abc123-cloudflare"

問題: If-None-Matchがオリジンと一致しなくなる。

解決策: CDN設定でオリジンのETagを尊重するよう設定。

// Cloudflare Workers で修正
const response = await fetch(request);
const originalEtag = response.headers.get('ETag');

// オリジンのETagを保持
return new Response(response.body, {
  headers: {
    ...response.headers,
    'ETag': originalEtag
  }
});

Range リクエストとの組み合わせ

If-Range ヘッダー

動画や大きなファイルの部分ダウンロードを継続する際に使用します。

GET /video.mp4 HTTP/1.1
Range: bytes=1000000-
If-Range: "abc123xyz"

サーバーの判定:

  • ETagが一致 → 206 Partial Content(続きを返す)
  • ETagが不一致 → 200 OK(全体を返す)

レスポンス(一致時):

HTTP/1.1 206 Partial Content
Content-Range: bytes 1000000-1999999/2000000
ETag: "abc123xyz"
Content-Length: 1000000

レスポンス(不一致時):

HTTP/1.1 200 OK
ETag: "def456uvw"
Content-Length: 2000000

実践メモ: If-Rangeを使わないと、ファイルが更新されたときに壊れたファイルになる可能性があります。動画配信では必須です。

パフォーマンス最適化

Cache-Control との併用

Cache-Control: max-age=3600
ETag: "abc123xyz"

フロー:

  1. 0〜3600秒: キャッシュから取得(サーバーアクセスなし)
  2. 3600秒後: If-None-Matchで検証 → 304レスポンス
  3. 内容変更時: 200レスポンス(新しいETag)

ハッシュ付きファイル名との使い分け

# ハッシュ付きファイル名
/assets/app.a1b2c3.js
Cache-Control: max-age=31536000, immutable
# ETag不要(ファイル名が変われば別ファイル)

# ハッシュなしファイル名
/style.css
Cache-Control: max-age=3600
ETag: "abc123xyz"
# ETagで効率的に検証

デバッグとトラブルシューティング

Chrome DevTools で確認

  1. Network タブ → Response Headers

    • ETagの値を確認
    • 304レスポンスか200レスポンスかを確認
  2. Size 列

    • (from cache): キャッシュから取得
    • 304: 304レスポンス(数百バイト)
    • 1.2 MB: 200レスポンス(全体)

cURLでテスト

# 初回リクエスト
curl -I https://example.com/style.css

# ETagを使って検証
curl -I -H "If-None-Match: \"abc123xyz\"" https://example.com/style.css

# Last-Modifiedを使って検証
curl -I -H "If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT" https://example.com/style.css

よくある問題

304が返らない:

  • ETagがリクエストごとに変わっていないか確認
  • 複数サーバーで異なるETagを返していないか確認
  • If-None-Matchヘッダーが正しく送信されているか確認

キャッシュが効きすぎる:

  • Cache-ControlのMax-Ageを短くする
  • HTMLにはno-cacheを使う

ベストプラクティス

1. 適切なリソースにETags を使用

リソースETagLast-ModifiedCache-Control
静的ファイル(hash付き)不要不要max-age=31536000, immutable
静的ファイル(hashなし)max-age=3600
HTMLno-cache
API不要private, max-age=60
動画・大ファイルmax-age=86400

2. 弱いETagを適切に使用

# 動的HTML(広告が変わる程度)
ETag: W/"abc123"

# 静的ファイル(完全一致が必要)
ETag: "abc123"

3. 楽観的ロックでデータ競合を防ぐ

// GETでETagを取得
const res = await fetch('/api/posts/123');
const post = await res.json();
const etag = res.headers.get('ETag');

// PUTでIf-Matchを送信
await fetch('/api/posts/123', {
  method: 'PUT',
  headers: {
    'If-Match': etag,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ title: 'Updated' })
});

4. 複数サーバーでETag一貫性を保つ

// ファイルハッシュベースのETag
const hash = crypto.createHash('md5').update(content).digest('hex');
const etag = `"${hash}"`;
// サーバーAでもサーバーBでも同じETag

注意: Nginxのデフォルトは時刻ベースのETagなので、ロードバランサー環境ではファイルハッシュベースに変更することを推奨します。

まとめ

ETagと条件付きリクエストは、HTTPキャッシュ戦略の中核です。If-None-Matchによる効率的な検証で304レスポンスを活用し、転送量を大幅に削減できます。CDN連携、楽観的ロック、Range リクエストとの組み合わせで、高速かつ安全なWebアプリケーションを実現しましょう。

参考リソース

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る