Menu

ヘルスチェックエンドポイント

注目すべき点

/api/health は v4.0.0 で追加されました。設定は不要で、デプロイすればすぐ利用できます。

契約

GET /api/health  ->  200  { status: 'ok',    at, uptimeSec, checks }
                     503  { status: 'error', at, uptimeSec, checks }

レスポンスの例:

{
  "status": "ok",
  "at": "2026-09-12T08:30:00.000Z",
  "uptimeSec": 86400,
  "checks": {
    "database": { "status": "ok", "latencyMs": 12 },
    "redis": { "status": "skipped", "latencyMs": 0 }
  }
}

checks の各項目は 3 つのステータスのいずれかを取ります。

ステータス意味全体への影響
okプローブの往復に成功なし
downプローブが失敗またはタイムアウトあり。全体が 503 になる
skippedこのデプロイでは未設定の依存なし

全体を赤にするのは down だけです。 そのため監視側は HTTP ステータスコードだけを見ればよく、ボディを解析する必要は一切ありません。

skipped は意図的な設計です。ボイラープレートは Redis なしで動作し、Redis を設定していないデプロイは完全に健全であって、それで警報を出してはいけません。

現在プローブしている依存

名前プローブ方法skipped になる条件
databaseselect 1データベースが無効な場合
redisGET health:probe(読み取り専用、書き込みなし)Redis クライアントが未設定の場合

Redis のプローブは固定の読み取り専用キーを使います。そのキーが存在するかどうかは無関係です —— 測っているのは往復であって値ではありません。

どちらのクライアントも「未設定なら null」であるため、その存在自体が設定の事実であり、コード内で環境変数の一覧を重複させる必要はありません。

2 つの設計判断

意図的に公開、意図的に寡黙

このエンドポイントはシークレットを一切必要とせず、どのプローブからも到達できます。ロードバランサー、uptime 監視、オーケストレーターが同じ契約を共有するため、いずれかに認証情報を要求すると面倒になるからです。

その代わり、詳細は何も返しません。エラー文字列も、バージョンも、ホスト名もありません。失敗理由はロガーにのみ送られます —— しかも error ではなく warn です。監視は数秒ごとに再プローブするため、error にすると実際の障害時に Sentry がプローブごとに例外で埋まってしまいます。503 自体が既に警報を上げているのですから。

すべてのプローブに上限がある

const CHECK_TIMEOUT_MS = 3_000

プローブは計算ではなくネットワークの往復です。3 秒が各依存に与えられる予算のすべてで、タイムアウトは down に劣化します。ハングした TCP 接続がプラットフォームに殺されるまでリクエストを保持し続けることはありません。

すべてのプローブは並列に実行されるため、総レイテンシは最も遅い依存であって、それらの合計ではありません。

監視への組み込み

uptime 監視(UptimeRobot / BetterStack / Pingdom など)

  • URL:https://あなたのドメイン/api/health
  • メソッド:GET
  • 期待するステータス:200
  • 推奨間隔:1〜5 分

キーワード一致や JSON の解析は不要で、ステータスコードだけで十分です。

コンテナオーケストレーション(Docker / Kubernetes)

docker-compose.yml

healthcheck:
  test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
  interval: 30s
  timeout: 5s
  retries: 3
  start_period: 40s

Kubernetes:

livenessProbe:
  httpGet:
    path: /api/health
    port: 3000
  initialDelaySeconds: 40
  periodSeconds: 30
readinessProbe:
  httpGet:
    path: /api/health
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 10

timeout は 5 秒以上を推奨します。単一の依存のプローブ予算だけで 3 秒あり、そこに並列スケジューリングとネットワークのオーバーヘッドが加わるため、詰めすぎると健全なインスタンスを落としてしまいます。

Coolify / Dokploy

どちらもアプリケーション設定にヘルスチェックの項目があります。/api/health を指定し、期待ステータスを 200 にしてください。

ロードバランサー / リバースプロキシ

Nginx の upstream ヘルスチェックや、クラウドの ALB/NLB ターゲットグループのヘルスチェックも同様に /api/health を指し、HTTP 200 で判定します。

依存を追加する

app/api/health/route.tsCHECKS 配列に 1 項目を追加するだけです。ほかの場所に分岐は生まれません。

app/api/health/route.ts
const CHECKS: HealthCheck[] = [
  {
    name: 'database',
    probe: isDatabaseEnabled ? () => db.execute(sql`select 1`) : null,
  },
  {
    name: 'redis',
    probe: redisClient ? () => redisClient.get(REDIS_PROBE_KEY) : null,
  },
  // 追加:probe は Promise を返す。解決すれば健全、例外を投げれば down。
  // probe が null なら、このデプロイではその依存が未設定(skipped)。
  {
    name: 'r2',
    probe: r2Client ? () => r2Client.headBucket({ Bucket: bucketName }) : null,
  },
]

3 つの約束:

  1. probe は解決すれば健全、例外を投げれば不健全で、戻り値は無視されます
  2. probenull なら「このデプロイでは未設定」を意味し、失敗ではなく skipped として報告されます
  3. プローブは読み取り専用でなければなりません。ヘルスチェックは高頻度で呼ばれるため、書き込みはすべて負債になります

よくある質問

Q: データベースが落ちたとき、なぜ 500 ではなく 503 なのですか?

503 Service Unavailable は「一時的に利用不可」を意味し、これこそロードバランサーやオーケストレーターが期待するシグナルです —— そのインスタンスを流量から外し、回復を待ちます。500 は「このリクエストが失敗した」を意味し、意味論が合いません。

Q: このエンドポイントは認証の後ろに置くべきですか?

推奨しません。機密情報を返さない一方、認証を付けると大半の監視ツールとの連携が複雑になります。コンプライアンス上どうしても必要な場合は、リバースプロキシ層で送信元 IP により制限してください。

Q: デプロイの成否判定に使えますか?

使えます。liveness(プロセスが生きている)と readiness(依存が応答する)の両方を兼ねます。デプロイスクリプトは 200 が返るまでポーリングしてから流量を切り替えられます。

Q: /api/cron/credits の GET とは何が違いますか?

あちらの GET は定期実行ルート自身のヘルスチェックにすぎず、依存を一切プローブせず、用途がまったく異なります。定期実行の本来の入口は POST です。定期実行タスクを参照してください。