ヘルスチェックエンドポイント
注目すべき点
/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 になる条件 |
|---|---|---|
database | select 1 | データベースが無効な場合 |
redis | GET 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: 40sKubernetes:
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 40
periodSeconds: 30
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 10timeout は 5 秒以上を推奨します。単一の依存のプローブ予算だけで 3 秒あり、そこに並列スケジューリングとネットワークのオーバーヘッドが加わるため、詰めすぎると健全なインスタンスを落としてしまいます。
Coolify / Dokploy
どちらもアプリケーション設定にヘルスチェックの項目があります。/api/health を指定し、期待ステータスを 200 にしてください。
ロードバランサー / リバースプロキシ
Nginx の upstream ヘルスチェックや、クラウドの ALB/NLB ターゲットグループのヘルスチェックも同様に /api/health を指し、HTTP 200 で判定します。
依存を追加する
app/api/health/route.ts の CHECKS 配列に 1 項目を追加するだけです。ほかの場所に分岐は生まれません。
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 つの約束:
probeは解決すれば健全、例外を投げれば不健全で、戻り値は無視されますprobeがnullなら「このデプロイでは未設定」を意味し、失敗ではなくskippedとして報告されます- プローブは読み取り専用でなければなりません。ヘルスチェックは高頻度で呼ばれるため、書き込みはすべて負債になります
よくある質問
Q: データベースが落ちたとき、なぜ 500 ではなく 503 なのですか?
503 Service Unavailable は「一時的に利用不可」を意味し、これこそロードバランサーやオーケストレーターが期待するシグナルです —— そのインスタンスを流量から外し、回復を待ちます。500 は「このリクエストが失敗した」を意味し、意味論が合いません。
Q: このエンドポイントは認証の後ろに置くべきですか?
推奨しません。機密情報を返さない一方、認証を付けると大半の監視ツールとの連携が複雑になります。コンプライアンス上どうしても必要な場合は、リバースプロキシ層で送信元 IP により制限してください。
Q: デプロイの成否判定に使えますか?
使えます。liveness(プロセスが生きている)と readiness(依存が応答する)の両方を兼ねます。デプロイスクリプトは 200 が返るまでポーリングしてから流量を切り替えられます。
Q: /api/cron/credits の GET とは何が違いますか?
あちらの GET は定期実行ルート自身のヘルスチェックにすぎず、依存を一切プローブせず、用途がまったく異なります。定期実行の本来の入口は POST です。定期実行タスクを参照してください。