🚀

Cloudflare Hyperdrive導入で「Too many subrequests」エラーを根本解決した話

に公開

対象読者: Cloudflare Workersを使用している開発者、サーバーレスアーキテクチャでデータベース接続に課題を抱えている開発者


はじめに

Cloudflare WorkersNeon PostgreSQLを使用していた際、「Too many subrequests」エラーに悩まされていました。この記事では、その課題と解決策としてCloudflare Hyperdriveを導入した経緯、具体的な実装手順を紹介します。

起きた課題

症状

作っていたCronジョブが失敗します。Cloudflare Observabilityのログには以下のエラーが大量に記録されていました:

Error: Too many subrequests.

原因の分析

1. Neon Serverless Driverの制限

当時、Neon PostgreSQLに接続するために@neondatabase/serverlessを使用していました。このドライバーは、Cloudflare Workers環境では各SQLクエリをHTTP REST APIリクエストとして送信します。

つまり:

import { neon } from '@neondatabase/serverless';
const sql = neon(DATABASE_URL);

// 各クエリが1つのsubrequestを消費
const result1 = await sql`SELECT ...`;  // subrequest #1
const result2 = await sql`SELECT ...`;  // subrequest #2
const result3 = await sql`SELECT ...`;  // subrequest #3
// ...

2. Cloudflare Workersのsubrequest制限

  • Freeプラン: 50-100 subrequests/request
  • Paidプラン: 1,000 subrequests/request

3. subrequest消費量

ある処理では、以下のような実行なされていました:

  • 機能A(DBへのアクセス): 約10-15回のSQLクエリ
  • 機能B(DBへの保存): 約5-10回のSQLクエリ
  • 機能C(DBへの保存): 約5-10回のSQLクエリ
  • 合計: 20-30回のSQLクエリ = 20-30 subrequests

これに加えて、APIの呼び出しもsubrequestとしてカウントされるため、Freeプランの制限(50-100)を容易に超過していました。

試した対策と限界

コード最適化

  • 企業名抽出処理の無効化
  • API呼び出し上限の削減
  • ある機能の処理を条件に応じていくつかスキップ

これにより、subrequest数を60-90削減できましたが、依然として制限に近い状態でした。

GitHub Actionsへの移行は検討したが...

重い処理をGitHub Actionsに移行しようとましたが、何故かGitHub ActionsをScheduleで動かすことが出来ず、GitHub側へ問い合わせても判然とせず、別の方法を必要としました。

Hyperdrive導入に至った経緯

Hyperdriveとは

Cloudflare Hyperdriveは、Cloudflareが提供するデータベース接続加速サービスです。主な特徴は以下の通りです:

  1. データベース接続がsubrequestを消費しない

    • これが最大のメリットです。
    • データベース接続がsubrequestとしてカウントされないため、制限に達しにくくなります。
  2. 接続プーリング

    • 複数の接続を効率的に管理し、パフォーマンスを向上させます
  3. 自動キャッシング

    • 読み取りクエリを自動的にキャッシュし、レスポンス時間を短縮します
  4. Freeプランでも利用可能

    • 制限はありますが、Freeプランでも使用できます
      • アカウントごとに最大10構成
      • 構成あたり約20接続

なぜHyperdriveを選んだか

1. 根本解決

コード最適化やGitHub Actionsへの移行は、subrequest数を削減する対策でしたが、根本的な解決ではありませんでした。Hyperdriveを導入することで、データベース接続がsubrequestを消費しなくなり、根本的な問題を1つ解決できます。

2. コスト削減

当初は、CloudflareのPaidプラン($5/月)にアップグレードしてsubrequest制限を1,000に増やすことを検討していました。しかし、HyperdriveはFreeプランでも利用できるため、追加コストなしで問題を解決できます。

3. パフォーマンス向上

接続プーリングと自動キャッシングにより、データベース接続のパフォーマンスも向上します。


具体的な導入方法・手順

ステップ1: Hyperdrive設定の作成

Cloudflare DashboardまたはWrangler CLIを使用してHyperdrive設定を作成します。

方法A: Wrangler CLIを使用(推奨)

⚠️ セキュリティ警告: コマンドラインに直接接続文字列を指定すると、シェル履歴やプロセス一覧に残る可能性があります。本番環境では、以下の安全な方法を使用してください。

安全な方法1: 環境変数を使用

# 環境変数に接続文字列を設定(シェル履歴に残らない)
export DATABASE_URL="postgresql://user:password@host:port/database?sslmode=require"

# Staging環境用
wrangler hyperdrive create ai-news-db-staging \
  --connection-string="$DATABASE_URL" \
  --update-config \
  --env staging

# Production環境用
wrangler hyperdrive create ai-news-db-production \
  --connection-string="$DATABASE_URL" \
  --update-config \
  --env production

安全な方法2: 一時ファイルを使用(推奨)

# 一時ファイルに接続文字列を保存(権限を制限)
echo "postgresql://user:password@host:port/database?sslmode=require" > /tmp/db_url.txt
chmod 600 /tmp/db_url.txt

# ファイルから読み込んで使用
wrangler hyperdrive create ai-news-db-staging \
  --connection-string="$(cat /tmp/db_url.txt)" \
  --update-config \
  --env staging

# 使用後は即座に削除
rm /tmp/db_url.txt

非推奨: コマンドラインに直接指定(開発環境のみ)

# ⚠️ 警告: この方法は開発環境でのみ使用してください
# 本番環境では使用しないでください(シェル履歴に残るリスク)
wrangler hyperdrive create ai-news-db-staging \
  --connection-string="postgresql://user:password@host:port/database?sslmode=require" \
  --update-config \
  --env staging

注意: wrangler secret getコマンドは値を表示しないため、DATABASE_URLを環境変数やファイルから取得する必要があります。本番環境では、方法B(Cloudflare Dashboard)を使用することを強く推奨します。

方法B: Cloudflare Dashboardを使用

  1. Cloudflare Dashboardにログイン
  2. Workers & Pages > Hyperdrive に移動
  3. "Create a Hyperdrive" をクリック
  4. 設定名を入力(例: ai-news-db-staging
  5. Connection StringにDATABASE_URLを入力
  6. "Create" をクリック

ステップ2: wrangler.tomlの更新

Hyperdrive設定を作成後、返されたIDをwrangler.tomlに設定します:

compatibility_date = "2026-01-27"  # Node.js built-in modules対応
compatibility_flags = ["nodejs_compat"]  # pgライブラリ動作に必要

[env.staging]
[[env.staging.hyperdrive]]
id = "作成されたHyperdrive ID"
binding = "DB"

[env.production]
[[env.production.hyperdrive]]
id = "作成されたHyperdrive ID"
binding = "DB"

ステップ3: データベースクライアントの変更

Neon Serverless Driverからpgライブラリに移行します。

package.jsonの更新

{
  "dependencies": {
    "pg": "^8.11.3",
    // "@neondatabase/serverless": "^1.0.2" を削除
  },
  "devDependencies": {
    "@types/pg": "^8.11.6"
  }
}

src/db/client.tsの実装

import { Client } from 'pg';

let hyperdriveBinding: any = null;

export function setHyperdriveBinding(binding: any) {
  hyperdriveBinding = binding;
}

async function executeWithClient<T>(
  callback: (client: Client) => Promise<T>
): Promise<T> {
  let client: Client | null = null;
  
  try {
    // Hyperdriveが利用可能な場合は優先的に使用
    if (hyperdriveBinding?.connectionString) {
      const hyperdriveConnectionString = hyperdriveBinding.connectionString;
      console.log(`[DB Client] Using Hyperdrive connection`);
      client = new Client({
        connectionString: hyperdriveConnectionString,
         // SSL設定: Hyperdrive経由ではCloudflareの内部ネットワークを使用するため、
        // 証明書検証をスキップしてもリスクは低いが、セキュリティを考慮して
        // 接続文字列にsslmode=requireが含まれている場合はSSL接続を有効にする
        ssl: true, // SSL接続を有効にし、証明書検証も行う(デフォルト動作)
      });
    } else {
      // フォールバック: 通常のDATABASE_URLを使用
      const databaseUrl = process.env?.DATABASE_URL;
      if (!databaseUrl) {
        throw new Error('DATABASE_URL environment variable is required');
      }
      
      console.log(`[DB Client] Using DATABASE_URL (fallback mode)`);
      client = new Client({
        connectionString: databaseUrl,
        // SSL設定: 本番環境では証明書検証を有効にする(セキュリティのため)
        // Neonの接続文字列にはsslmode=requireが含まれているため、SSL接続は必須
        ssl: {
          rejectUnauthorized: true, // 証明書検証を有効にする(デフォルト)
        },
      });
    }
    
    await client.connect();
    return await callback(client);
  } finally {
    if (client) {
      await client.end();
    }
  }
}

// sqlテンプレートタグの互換性レイヤー
function createSqlTemplateTag() {
  const sqlTag = async (strings: TemplateStringsArray, ...values: any[]) => {
    return executeWithClient(async (client) => {
      let query = '';
      let paramIndex = 1;
      const params: any[] = [];
      
      for (let i = 0; i < strings.length; i++) {
        query += strings[i];
        if (i < values.length) {
          query += `$${paramIndex}`;
          params.push(values[i]);
          paramIndex++;
        }
      }
      
      const result = await client.query(query, params);
      return result.rows;
    });
  };
  
  (sqlTag as any).unsafe = async (query: string) => {
    return executeWithClient(async (client) => {
      const result = await client.query(query);
      return result.rows;
    });
  };
  
  return sqlTag;
}

let cachedSqlTag: ReturnType<typeof createSqlTemplateTag> | null = null;

export function getDbClient() {
  if (!cachedSqlTag) {
    cachedSqlTag = createSqlTemplateTag();
  }
  return cachedSqlTag;
}

ポイント:

  • sqlテンプレートタグの互換性レイヤーを実装することで、既存のコードを変更不要にしています
  • Hyperdriveが利用可能な場合は優先的に使用し、不可の場合はフォールバックします
  • SSL設定を明示的に指定します(Neonでは必須)
  • セキュリティ: 証明書検証を有効にすることで、中間者攻撃(MITM)を防止します

ステップ4: Workerエントリーポイントの変更

src/worker.tsでHyperdrive bindingを設定します:

import { setHyperdriveBinding } from './db/client.js';

export default {
  fetch: (request: Request, env: Record<string, any>, ctx: ExecutionContext) => {
    // Hyperdrive bindingが利用可能な場合は優先的に使用
    if (env?.DB) {
      setHyperdriveBinding(env.DB);
    }
    return app.fetch(request, envWithCtx, ctx);
  },
  scheduled: (event: any, env: Record<string, any>, ctx: ExecutionContext) => {
    // Hyperdrive bindingが利用可能な場合は優先的に使用
    if (env?.DB) {
      setHyperdriveBinding(env.DB);
    }
    // ... 既存のCron処理
  },
};

ステップ5: APIミドルウェアの変更

src/api/index.tsでHyperdrive bindingを設定します:

import { setHyperdriveBinding } from '../db/client.js';

app.use('/*', (c, next) => {
  const env = c.env as Record<string, any>;
  
  // Hyperdrive bindingが利用可能な場合は優先的に使用
  if (env?.DB) {
    setHyperdriveBinding(env.DB);
  }
  
  return next();
});

ステップ6: データベース権限の設定

Hyperdrive用のデータベースユーザー(hyperdrive_user)に必要な権限を付与します。

Neonデータベースで以下のSQLを実行してください:

-- すべてのテーブルに対する権限を付与
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO hyperdrive_user;

-- すべてのシーケンスに対する権限を付与(SERIAL型のカラムで使用)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO hyperdrive_user;

-- 将来作成されるテーブルとシーケンスに対するデフォルト権限を設定
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO hyperdrive_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO hyperdrive_user;

ステップ7: デプロイとテスト

# 依存関係のインストール
npm install

# Staging環境にデプロイ
npx wrangler deploy --env staging

# Production環境にデプロイ
npx wrangler deploy --env production

ステップ8: 動作確認

  1. ログの確認

    • Cloudflare Observabilityでログを確認
    • [DB Client] Using Hyperdrive connectionが表示されていることを確認
  2. エラーの確認

    • 「Too many subrequests」エラーが発生していないことを確認
    • データベース権限エラーが発生していないことを確認
  3. パフォーマンスの確認

    • データベース接続のレスポンス時間が改善されていることを確認

導入効果

1. subrequests問題の根本解決

導入前:

  • データベース接続: 20-30 subrequests
  • その他のAPI呼び出し: 20-30 subrequests
  • 合計: 40-60 subrequests(Freeプランの制限50-100に近い)

導入後:

  • データベース接続: 0 subrequests(Hyperdrive経由)
  • その他のAPI呼び出し: 20-30 subrequests
  • 合計: 20-30 subrequests(制限に余裕がある)

2. パフォーマンス向上

  • 接続プーリングにより、接続確立時間が短縮
  • 自動キャッシングにより、読み取りクエリのレスポンス時間が短縮

3. コスト削減

  • Freeプランで利用可能なため、追加コストなし
  • Paidプランへのアップグレードが不要

まとめ

Cloudflare Hyperdriveを導入することで、「Too many subrequests」エラーを根本的に解決できました。Freeプランでも利用できるため、追加コストなしで問題を解決できます。

主なポイント:

  1. Hyperdriveはデータベース接続がsubrequestを消費しない
  2. Freeプランでも利用可能(制限あり)
  3. 既存コードとの互換性を保つために、sqlテンプレートタグの互換性レイヤーを実装
  4. データベース権限の設定が重要

参考資料:


トラブルシューティング

1. SSL接続エラー

エラー:

connection is insecure (try using sslmode=require)

解決策:
pg.ClientのコンストラクタでSSL設定を明示的に指定します。

// Hyperdrive経由の場合
ssl: true, // SSL接続を有効にし、証明書検証も行う

// フォールバック(直接DATABASE_URL)の場合
ssl: {
  rejectUnauthorized: true, // 証明書検証を有効にする(セキュリティのため)
}

注意: 本番環境ではrejectUnauthorized: falseは使用しないでください。これは証明書検証をスキップするため、中間者攻撃(MITM)に対して脆弱になります。Neonの接続文字列にはsslmode=requireが含まれているため、SSL接続は必須ですが、証明書検証も有効にすることを推奨します。

2. Node.js built-in module解決エラー

エラー:

Could not resolve "fs"
Could not resolve "stream"
Could not resolve "string_decoder"

解決策:
wrangler.tomlで以下を設定します:

compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]

3. データベース権限エラー

エラー:

permission denied for sequence news_days_id_seq

解決策:
hyperdrive_userに必要な権限を付与します(ステップ6を参照)。


この記事が、同じ課題に直面している開発者の参考になれば幸いです。

Discussion