Cloudflare Hyperdrive導入で「Too many subrequests」エラーを根本解決した話
対象読者: Cloudflare Workersを使用している開発者、サーバーレスアーキテクチャでデータベース接続に課題を抱えている開発者
はじめに
Cloudflare WorkersでNeon 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が提供するデータベース接続加速サービスです。主な特徴は以下の通りです:
-
データベース接続がsubrequestを消費しない
- これが最大のメリットです。
- データベース接続がsubrequestとしてカウントされないため、制限に達しにくくなります。
-
接続プーリング
- 複数の接続を効率的に管理し、パフォーマンスを向上させます
-
自動キャッシング
- 読み取りクエリを自動的にキャッシュし、レスポンス時間を短縮します
-
Freeプランでも利用可能
- 制限はありますが、Freeプランでも使用できます
- アカウントごとに最大10構成
- 構成あたり約20接続
- 制限はありますが、Freeプランでも使用できます
なぜ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を使用
- Cloudflare Dashboardにログイン
- Workers & Pages > Hyperdrive に移動
- "Create a Hyperdrive" をクリック
- 設定名を入力(例:
ai-news-db-staging) - Connection StringにDATABASE_URLを入力
- "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: 動作確認
-
ログの確認
- Cloudflare Observabilityでログを確認
-
[DB Client] Using Hyperdrive connectionが表示されていることを確認
-
エラーの確認
- 「Too many subrequests」エラーが発生していないことを確認
- データベース権限エラーが発生していないことを確認
-
パフォーマンスの確認
- データベース接続のレスポンス時間が改善されていることを確認
導入効果
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プランでも利用できるため、追加コストなしで問題を解決できます。
主なポイント:
- Hyperdriveはデータベース接続がsubrequestを消費しない
- Freeプランでも利用可能(制限あり)
- 既存コードとの互換性を保つために、
sqlテンプレートタグの互換性レイヤーを実装 - データベース権限の設定が重要
参考資料:
トラブルシューティング
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