---
title: "コネクションプールに注意：serverless から DB につなぐときの小さな落とし穴"
description: "従来のバックエンドでは常識だったコネクションプールも、serverless に持ってくると地雷になります。"
author: "kidneyweakx"
published: 2025-12-20T11:28:43.101Z
updated: 2026-10-29T10:17:47.595Z
lang: ja
tags: ["serverless", "cloudflare", "database", "postgres"]
canonical: https://kidneyweakx.com/blog/serverless-db-connection-pitfalls?lang=ja
alternate_zh_tw: https://kidneyweakx.com/blog/serverless-db-connection-pitfalls.md
alternate_en: https://kidneyweakx.com/blog/serverless-db-connection-pitfalls.md?lang=en
---

# コネクションプールに注意：serverless から DB につなぐときの小さな落とし穴

> 従来のバックエンドでは常識だったコネクションプールも、serverless に持ってくると地雷になります。

「客家エンジニアの生存法則」の記事で少し触れましたが、書き終えてみると「爆発する」としか言っておらず、「なぜ爆発するのか、どう書けば爆発しないのか」には触れていませんでした。この記事ではこの落とし穴をしっかり掘り下げて、ついでにそのままコピペできる書き方もまとめておきます。

<br />

## コネクションプールの前提：プログラムがずっと生きていること

従来の Node / Go / Java のバックエンドだと、コネクションプールはだいたいこんな感じです。

```ts
// server.ts —— 開機時建一次，之後所有 request 共用
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 })

app.get('/posts', async () => {
  const { rows } = await pool.query('select * from posts')
  return rows
})
```

これが動くのは、明文化されていないひとつの前提があるからです。**この process は長く生き続ける**、ということ。起動時に少し時間をかけて TCP 接続を 10 本張っておけば、あとは 1 万件の request が順番に借りては返すだけで、DB 側から見える接続はずっと 10 本のままです。

serverless は、この前提を丸ごと取り払ってしまいます。

## Workers ではどう壊れるか

上のような書き方を初めて Cloudflare Workers に持っていったとき、直感的に「module scope で pool を 1 回作っておけば、今までと同じでしょ」と思いました。結果、3 つの問題にぶつかります。

**1. 接続を request をまたいで再利用できない**

Workers の I/O オブジェクト（TCP socket を含む）は、それを作った request に紐づいています。前の request が開いた接続を次の request で使おうとすると、いきなりこう怒られます。

```
Error: Cannot perform I/O on behalf of a different request.
```

つまり、**module scope の pool は、Workers ではそもそも元のやり方で共有できません**。

**2. インスタンス数は自分で決められない**

request をまたいで接続を保持できる serverless プラットフォームであっても、トラフィックが来ればプラットフォームが一気にインスタンスを増やし、それぞれが自分の pool を持ちます。インスタンス 10 個 × 各 10 本で、接続は 100 本。Postgres のデフォルトの `max_connections` は 100 なので、管理画面や migration、cron の分はまだ計算にすら入っていません。

**3. インスタンスが回収されるとき、接続がちゃんと閉じられるとは限らない**

インスタンスは容赦なく回収されます。`end()` されなかった接続は DB 側に残り、timeout になるまで掃除されません。ピークが過ぎると DB にはゾンビ接続が大量にぶら下がっていて、次のトラフィックの波はそのまま拒否されます。

<br />

**一番つらいのは、普段はすべて正常で、トラフィックが多いときだけ爆発することです。** そしてトラフィックが多いときこそ、一番生きていてほしいときなんですよね。

## 解決策 1：Hyperdrive で、コネクションプールを DB の隣に移す

どうしても Postgres を使いたいなら、Cloudflare の Hyperdrive が一番手間のかからない解決策です。Cloudflare 側でコネクションプールを管理してくれるので、Worker は request ごとに DB へ直接つなぐのではなく、Hyperdrive につなぐだけで済みます。

ポイントは、**client は request の中で作り、使い終わったら `waitUntil` の中で閉じる**ことです。

```ts
import postgres from 'postgres'

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // 每個 request 自己建 client；真正的連接池在 Hyperdrive 那一側
    const sql = postgres(env.HYPERDRIVE.connectionString, { max: 5, fetch_types: false })

    const posts = await sql`select id, title from posts order by published_at desc limit 20`

    // 回應送出後再關連線，不擋住使用者
    ctx.waitUntil(sql.end())
    return Response.json(posts)
  },
}
```

`wrangler.jsonc` に binding を 1 つ追加します。

```jsonc
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<your-hyperdrive-id>" }]
```

こうすれば、DB から見える接続数は Hyperdrive の pool のサイズだけになり、Worker のインスタンス数と一緒に膨れ上がることはなくなります。

## 解決策 2：HTTP 経由のデータベース driver

もう 1 つの道は、いっそ長時間つなぎっぱなしの接続を使わないことです。Turso（libSQL）や Neon の serverless driver なら、クエリ 1 回をそのまま 1 回の HTTP リクエストにできます。

```ts
import { createClient } from '@libsql/client/web'

export default {
  async fetch(request: Request, env: Env) {
    const db = createClient({ url: env.TURSO_URL, authToken: env.TURSO_TOKEN })
    const { rows } = await db.execute('select id, title from posts limit 20')
    return Response.json(rows)
  },
}
```

管理すべき TCP 接続がないので、コネクションプールの問題もありません。その代わり、クエリごとに HTTP のレイテンシが少し増えますし、トランザクション（transaction）の書き方もその driver がサポートする形に変える必要があります。このブログも以前 Turso につないでいたときは、この方法でした。

## 解決策 3：そもそもデータベースを使わない

最後の道は一番力技ですが、個人プロジェクトの多くには実はこれが一番合っています。**データ量が少なく、書き込み頻度も低いなら、データベースはいらない**。

このブログが今まさにそうです。記事一覧は R2 上の `index.json` ひとつ、本文は Markdown ファイルの集まりで、フロント側は R2 の読み取り専用権限しか持っていません。接続もなければ pool もなく、`max_connections` もない。なので、この記事で話している落とし穴はひとつも存在しません。

<br />

## どう選ぶか

| 状況 | おすすめ |
|---|---|
| すでに Postgres があり、リレーショナルなクエリやトランザクションが必要 | Hyperdrive + request ごとに client を作る |
| 新規プロジェクト、データは複雑でなく、インフラ管理を減らしたい | Turso / Neon のような HTTP driver |
| 個人サイト、書き込みは月に数回 | データベースは不要、KV か R2 + JSON で十分 |

## ひとことでまとめると

**コネクションプールは「長生きする process」のために設計されたものです。** serverless の process は長生きしないうえに、数も自分では管理できません。だから pool はプラットフォームに任せる（Hyperdrive）か、pool のいらない接続方法に変える（HTTP driver）か、いっそデータベースごとやめるか、のどれかです。

ここをきちんと押さえておけば、トラフィックが一番多い夜に `too many connections` を眺めながら人生を見つめ直す羽目にならずに済みます。
