Cloudflare Containersをウォームに保つ:ヘルスチェックエンドポイントでコールドスタートを排除

Fred· AI Engineer & Developer Educator3 min read

最終テスト日: 2025年12月 | プラットフォーム: Cloudflare Workers Containers | 言語: Node.js、Python

Cloudflare Workers Containersは印象的です。完全なコンテナランタイム—任意の言語、任意のフレームワーク—がCloudflareのグローバルネットワーク全体に複製・分散されます。ウォーム状態になると、ほとんどレイテンシなしで応答します。問題は、最初にウォーム状態にすることです。

基本的なWebサーバーより重いものを実行している場合、コールドスタートは3〜5秒以上になる可能性があります。これは、その日の最初のユーザーや、静かな期間の後の最初のリクエストにとっては永遠です。ミリ秒で起動する標準のWorkersとは異なり、コンテナはイメージをプル、ランタイムを起動し、アプリケーションを初期化する必要があります。これを完全に排除する魔法の弾丸はありませんが、コールドスタートをほとんどのユーザーが経験しないほど稀にすることはできます。

解決策は、AWS LambdaやGCP Cloud Functionsで何年も機能してきたのと同じパターンです:スケジュールされたpingでコンテナをウォームに保ちます。

コールドスタートの実際のコスト

Cloudflare Containerがコールドになると、次のリクエストは一連の操作をトリガーします。コンテナイメージをプルする必要があります(近くにキャッシュされていない場合)。ランタイムが起動し、アプリケーションがブートします。初期化コードが実行されます—データベース接続がオープンし、設定ファイルが解析され、モデルがメモリにロードされます。その後ようやく実際のリクエストハンドラが実行されます。

シンプルなNode.jsサーバーの場合、これは1〜2秒追加されるかもしれません。画像処理ライブラリなどの重い依存関係を持つPythonアプリケーションの場合、5秒以上になることも簡単です。Redditのあるユーザーは、画像プロセッサコンテナで「アップロードパケットが送信されるまで最大5秒」を報告していました—それも大きなコンテナサイズの一つでした。

最初のバイト遅延は単に迷惑なだけではありません。知覚されるパフォーマンスを殺します。3秒以内に応答しないページはユーザーに放棄されます。タイムアウトするAPIは壊れているように見えます。コンテナは実行中は非常に高速かもしれませんが、コールドスタートがすでにユーザーを追い払っていたら、それは関係ありません。

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

修正は簡単です:コンテナが即座に応答できる軽量なエンドポイントを追加し、外部からスケジュールでpingします。これにより、少なくとも1つのコンテナインスタンスがウォームで実際のトラフィックに対応できる状態を保ちます。

ヘルスチェックエンドポイントの重要なルールは、重い依存関係を初期化してはならないことです。画像ライブラリなし、データベース接続なし、モデルローディングなし、キャッシュウォーミングなし。最小限の作業で即座に返す必要があります—理想的には204 No Contentレスポンスだけです。ヘルスチェックが実際のエンドポイントと同じ初期化をトリガーすると、目的全体が台無しになります。

コンテナが実行中であることを証明する別のコードパスと考えてください。実際のリクエストは完全な初期化を取得します。ヘルスチェックリクエストは高速な「はい、生きています」を取得し、それ以外は何もしません。

Node.js実装

適切なヘルスチェックエンドポイントを持つ完全なNode.jsサーバーを示します。重い初期化が遅延ロードフラグの後ろでガードされ、実際のリクエストでのみトリガーされることに注目してください。

import http from 'node:http';

const PORT = process.env.PORT || 8080;

// 重い依存関係は必要になるまで初期化されない
let sharp;
let pipelineReady = false;

async function initializePipeline() {
  if (pipelineReady) return;

  // これが遅い部分 - 実際のリクエストでのみ実行
  sharp = (await import('sharp')).default;
  // 他の高コストなセットアップもここに
  pipelineReady = true;
  console.log('Pipeline initialized');
}

const server = http.createServer(async (req, res) => {
  // ヘルスチェック:即座に返し、すべての初期化をスキップ
  if (req.method === 'GET' && req.url === '/healthcheck') {
    res.writeHead(204, {
      'cache-control': 'no-store',
      'content-type': 'text/plain'
    });
    return res.end();
  }

  // 実際のリクエスト:必要に応じて依存関係を初期化
  await initializePipeline();

  // 実際のリクエスト処理はここに
  if (req.method === 'POST' && req.url === '/resize') {
    // sharpで画像を処理...
    res.writeHead(200, { 'content-type': 'application/json' });
    return res.end(JSON.stringify({ success: true }));
  }

  res.writeHead(404, { 'content-type': 'text/plain' });
  res.end('Not found');
});

server.listen(PORT, () => {
  // ここで重い作業をしない - コールドスタート時に実行される
  console.log(`Server listening on port ${PORT}`);
});

ヘルスチェックパスはマイクロ秒で204レスポンスを返します。sharpインポートや他の高コストな依存関係には触れません。/resizeで実際のリクエストが来たとき、そのときに初めてinitializePipeline()が実行されます。

Expressを使用している場合、パターンは同じですがExpressルーティングを使用します:

import express from 'express';

const app = express();
const PORT = process.env.PORT || 8080;

let pipelineReady = false;

async function ensurePipeline(req, res, next) {
  if (req.path === '/healthcheck') return next();

  if (!pipelineReady) {
    // 重い依存関係を遅延ロード
    const sharp = (await import('sharp')).default;
    app.locals.sharp = sharp;
    pipelineReady = true;
  }
  next();
}

app.use(ensurePipeline);

// ヘルスチェックは上記のチェック以外のすべてのミドルウェアをバイパス
app.get('/healthcheck', (req, res) => res.status(204).end());

// 実際のエンドポイントは初期化されたパイプラインを取得
app.post('/resize', async (req, res) => {
  const sharp = req.app.locals.sharp;
  // sharpで処理...
  res.json({ success: true });
});

app.listen(PORT, () => console.log(`Listening on ${PORT}`));

ミドルウェアensurePipelineはヘルスチェックにヒットしているかチェックし、そうであれば初期化をスキップします。他のすべてのルートは完全な依存関係ロードを取得します。これにより、ヘルスチェックは高速に保たれ、実際のリクエストは必要なすべてを持っています。

Python実装

同じパターンがPythonでも機能します。遅延ロードを持つFastAPI実装を示します:

from fastapi import FastAPI, Response
from functools import lru_cache
import os

app = FastAPI()

# 重いインポートはモジュールレベルに留まるが実行されない
# 実際のロードはget_processor()で発生
_processor = None

def get_processor():
    global _processor
    if _processor is None:
        # これが遅い部分 - 重いインポートはここで発生
        from PIL import Image
        import numpy as np
        _processor = {'pil': Image, 'np': np}
        print('Processor initialized')
    return _processor

@app.get('/healthcheck')
def healthcheck():
    # 即座に返す - 初期化なし
    return Response(status_code=204)

@app.post('/resize')
async def resize():
    # 実際のリクエスト:必要に応じて初期化
    proc = get_processor()
    # proc['pil']とproc['np']を画像処理に使用...
    return {'success': True}

if __name__ == '__main__':
    import uvicorn
    port = int(os.environ.get('PORT', 8080))
    uvicorn.run(app, host='0.0.0.0', port=port)

get_processor()関数はグローバルフラグを使用して、高コストなインポートが一度だけ発生し、実際のエンドポイントが呼び出したときにのみ発生することを保証します。ヘルスチェックエンドポイントはこの初期化をトリガーしません—ただ204を返して終了します。

Cronワーカー:ウォームに保つ

コンテナにヘルスチェックエンドポイントがあります。次に、スケジュールでpingするものが必要です。最もクリーンな解決策は、Cron Triggerを持つCloudflare Workerです—Cloudflareのエコシステム内に完全に留まり、無料ティアでは何もコストがかかりません。

ウォーマー用の新しいWorkerプロジェクトを作成します:

mkdir container-warmer && cd container-warmer
npm init -y

cronスケジュールを持つwrangler.tomlを作成します:

name = "container-warmer"
main = "src/index.ts"
compatibility_date = "2025-12-01"

[triggers]
crons = ["*/5 * * * *"]

*/5 * * * *式は5分ごとに実行されます。コンテナをどれだけウォームに保つ必要があるかに応じて調整します—より頻繁なpingはより暖かいコンテナを意味しますが、Workerの呼び出し回数も増えます。

スケジュールされたハンドラを持つsrc/index.tsを作成します:

export interface Env {
  CONTAINER_URL: string;
  AUTH_TOKEN?: string;
}

export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(pingContainer(env));
  }
};

async function pingContainer(env: Env) {
  const headers: Record<string, string> = {
    'user-agent': 'cloudflare-container-warmer/1.0'
  };

  // オプション:ヘルスチェックをベアラートークンで保護
  if (env.AUTH_TOKEN) {
    headers['authorization'] = `Bearer ${env.AUTH_TOKEN}`;
  }

  try {
    const response = await fetch(env.CONTAINER_URL, {
      method: 'GET',
      headers
    });

    if (response.ok || response.status === 204) {
      console.log('Container warmed successfully');
    } else {
      console.log(`Warm ping returned status: ${response.status}`);
    }
  } catch (error) {
    console.log(`Warm ping failed: ${error}`);
  }
}

環境変数を設定してデプロイします:

# コンテナのヘルスチェックURLを設定
npx wrangler secret put CONTAINER_URL
# 入力: https://your-container.your-subdomain.workers.dev/healthcheck

# オプション:認証トークンを設定
npx wrangler secret put AUTH_TOKEN

# ウォーマーをデプロイ
npx wrangler deploy

Workerは5分ごとにコンテナにpingを送り、少なくとも1つのインスタンスをウォームでトラフィック準備完了の状態に保ちます。

推奨間隔

適切なping間隔は、コールドスタートに対する許容度とコストによって異なります:

2〜3分ごと: 非常にウォーム。コールドスタートは極めて稀になります。Workerの呼び出し回数が多くなります。

5分ごと: ほとんどのアプリケーションにとってスイートスポット。コンテナは一貫したパフォーマンスのために十分ウォームに保たれ、過度なpingなしで済みます。

10〜15分ごと: ほぼウォーム。静かな期間中に時折コールドスタートが見られますが、ウォーミングなしよりははるかに良いです。

30分以上ごと: 最小限のウォーミング。コンテナが完全に退去されるのを防ぎたいだけなら良いですが、コールドスタートは定期的に発生します。

5分から始めて、ログで見られることに基づいて調整してください。ユーザーが最初のロードが遅いと報告したら、間隔を短くします。コンテナがすでに安定したトラフィックを処理しているなら、ウォーミングは全く必要ないかもしれません。

すべてをまとめる

完全なワークフローは以下の通りです:

  1. 重い依存関係を初期化せずに即座に204を返す/healthcheckエンドポイントをコンテナに追加
  2. 5分ごとにヘルスチェックにpingを送るCron Trigger付きのWorkerをデプロイ
  3. オプションでヘルスチェックをベアラートークンで保護し、ウォーマーのみがアクセスできるようにする
  4. コンテナログを監視してpingが機能し、コールドスタートが減少していることを確認

実装には約20分かかります。その見返りは、すでにウォームな状態でヒットしたユーザーだけでなく、すべてのユーザーにとって高速に感じるコンテナです。

Cloudflare Containersは標準のWorkersと比較して安価ではありませんが、任意の言語、任意のフレームワーク、任意のランタイムで何でも実行できます。ウォームに保つことで、支払っているパフォーマンスを実際に得られることを保証します。

Fred

Fred

AUTHOR

Full-stack developer with 10+ years building production applications. I've been deploying to Cloudflare's edge network since Workers launched in 2017.

Need a developer who gets it?

POC builds, vibe-coded fixes, and real engineering. Let's talk.

Hire Me →