# Vercel Functions × Fluid Compute 実装ガイド：並行性・ストリーミング・waitUntil・Cron を本番品質で

> Vercel公式に忠実なFunctions実装ガイド。Fluid Compute（既定）の最適化された並行性とグローバル状態の落とし穴、Node.js/Python/Bun/Rustランタイム、ストリーミング、waitUntilの後処理、maxDuration/メモリ設定、Cron JobsのCRON_SECRET保護、グレースフルシャットダウンと冪等性までを実コードで体系化します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Vercel, Fluid Compute, サーバーレス, Next.js, TypeScript, 可観測性, コスト最適化
- URL: https://tomodahinata.com/blog/vercel-functions-fluid-compute-streaming-cron-guide
- カテゴリ: Vercel 本番運用
- 総合ガイド: https://tomodahinata.com/blog/vercel-production-platform-guide

## 要点

- Fluid Computeは『1インスタンスで複数リクエストを並行処理』するモデル。これによりコールドスタートとコストが下がる一方、複数リクエストがプロセス（グローバル状態）を共有するため、モジュールスコープにリクエスト固有の状態を置くと情報漏洩する。共有してよいのはDB接続プールなどリクエスト非依存のものだけ
- ランタイムはNode.js 24 LTS（既定）・Python・Edge・Bun・Rust。最適化された並行性が効くのはNode.jsとPython。I/Oバウンドな処理ほど並行性の恩恵が大きい
- waitUntilでレスポンス後のバックグラウンド処理（ログ・分析・Webhook転送）を実行できる。ユーザーへの応答を遅らせずに後処理を完了させる本番の定石
- ストリーミングはReadableStreamで実装。Edgeランタイムは25秒以内に応答開始し最大300秒ストリーム可能。長時間処理はWorkflowsへ切り離す
- Cron JobsはvercelのHTTP GETで起動。CRON_SECRETとAuthorizationヘッダで必ず保護し、x-vercel-cron-scheduleヘッダで複数ジョブを判別。タイムゾーンは常にUTC、冪等に設計する

---

Vercel Functions は「`api/` にファイルを置けば動く」ところまでは簡単です。難しいのは**本番品質**——並行リクエストでデータが混ざらないか、I/O 待ちでコストが膨らまないか、後処理を取りこぼさないか、Cron が無防備に叩かれないか。この記事は、[Vercel Functions](https://vercel.com/docs/functions) と [Fluid Compute](https://vercel.com/docs/fluid-compute) の公式仕様に忠実に、**落ちない・追える・安い関数**の作り方を実コードでまとめます。

全体像（コンピュート以外のレイヤー）は [Vercel 本番運用ガイド](/blog/vercel-production-platform-guide) を、課金の詳細は [Active CPU 最適化ガイド](/blog/vercel-cost-active-cpu-pricing-optimization-guide) を参照してください。本稿は「Functions をどう書くか」に集中します。

---

## Fluid Compute：1インスタンスで複数リクエスト

### なぜ速く・安くなるのか

従来のサーバーレスは「1リクエスト＝1インスタンス（microVM）」でした。これだと、

- リクエストのたびに**コールドスタート**が起こりうる
- 関数が DB や AI の応答を**待っている間**もインスタンスが1リクエストを占有し、遊ぶ

Fluid Compute は、**1つの関数インスタンスが複数の呼び出しを並行処理**します。公式の言葉では「最適化された並行性（optimized concurrency）」。I/O 待ちの間に同じインスタンスが別のリクエストを処理できるため、**コールドスタートが減り、必要なインスタンス総数が減り、コストが下がる**。AI（埋め込み・ベクトル検索・外部API）のような I/O バウンドな処理で特に効きます。

```ts
// app/api/recommend/route.ts
// I/O バウンドな処理の典型。Fluid Compute では、この await の「待ち時間」に
// 同じインスタンスが別リクエストを処理できる。
export async function POST(request: Request) {
  const { userId } = await request.json();

  // いずれも外部I/O（CPUはほぼ使わない＝Active CPU課金が増えない）
  const [embedding, profile] = await Promise.all([
    fetchEmbedding(userId),   // AI API
    db.users.findById(userId) // DB
  ]);

  const items = await vectorSearch(embedding); // ベクトルDB
  return Response.json({ items, profile });
}
```

### 最大の落とし穴：共有グローバル状態

Fluid Compute の本質は **「複数リクエストが同一プロセス＝グローバル状態を共有する」** ことです。これは性能上の利点であると同時に、**最も多いセキュリティバグの原因**になります。

```ts
// ❌ 危険：リクエスト固有のデータをモジュールスコープに置く
let currentUser: User | null = null; // 全リクエストで共有される！

export async function GET(request: Request) {
  currentUser = await authenticate(request); // 別リクエストが上書きする競合
  return Response.json(await getDashboard(currentUser)); // 他人のデータが混ざりうる
}
```

```ts
// ✅ 安全：リクエスト固有のデータは関数スコープに閉じる
export async function GET(request: Request) {
  const user = await authenticate(request); // ローカル変数
  return Response.json(await getDashboard(user));
}

// ✅ グローバルに置いてよいのは「リクエスト非依存」のものだけ
//    （DB接続プール・設定・コンパイル済みスキーマなど）
const pool = createPool(process.env.DATABASE_URL!);
```

> **規律**：モジュールスコープの `let`／可変オブジェクトに**ユーザー・トークン・テナント由来の値**を入れない。共有するのは「誰のリクエストでも同じで安全なもの」だけ。これは Node.js のサーバー実装と同じ規律ですが、サーバーレスからの移行組ほど見落とします。

### エラー隔離

Fluid Compute は、Node.js で**未処理例外（uncaughtException）・未処理 Rejection** が起きても、エラーをログして**実行中の他リクエストを完了させてから**プロセスを止めます。1つの壊れたリクエストが、同居する他リクエストを巻き込みません。とはいえ「握りつぶし」ではないので、**例外は各リクエスト内で適切に処理**するのが前提です。

---

## ランタイムを選ぶ

Fluid Compute は次のランタイムで動きます（[runtimes](https://vercel.com/docs/functions/runtimes)）。

| ランタイム | 最適化された並行性 | 使いどころ |
|---|---|---|
| **Node.js 24 LTS（既定）** | ✅ | 大半のアプリ。フル Node.js API。Node 18 は非推奨 |
| **Python**（3.13/3.14） | ✅ | FastAPI 等。データ/ML 周辺 |
| **Edge** | — | 軽量・極小レイテンシ。ただし互換性に難（**新規は基本 Fluid/Node 推奨**） |
| **Bun** | — | Bun ネイティブな処理 |
| **Rust** | — | CPU 集約・低レイテンシが要る部分 |

> 2026年の指針：**「速くしたいから Edge」ではなく、まず Fluid Compute（Node.js）**。Edge と Middleware は内部的に Vercel Functions で動いており、Fluid は同一リージョン・同一価格で通常の Node.js を使えます。

### maxDuration とメモリを明示する

既定タイムアウトは**全プラン300秒**。Pro/Ent は800秒（GA）まで設定可、拡張1800秒はベータ（関数単位設定）。**既定の放置は避け**、用途に合わせて明示します。

```ts
// app/api/report/route.ts
export const maxDuration = 60; // この関数は最大60秒（秒単位）
export const runtime = "nodejs"; // 既定。明示しておくと意図が伝わる

export async function GET() {
  return Response.json(await buildHeavyReport());
}
```

設定の優先順位は **関数コード ＞ vercel.json ＞ ダッシュボード ＞ Fluid 既定**。コードに書いた値が最優先です。メモリは Pro/Ent で最大 4GB/2vCPU、Hobby は 2GB/1vCPU。

---

## ストリーミング：最初の1バイトを速く返す

LLM の生成やレポートの逐次出力は、**完成を待たずに少しずつ返す**とUXが激変します。標準の `ReadableStream` で実装できます。

```ts
// app/api/stream/route.ts — テキストを逐次ストリーム
export async function GET() {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      for (const chunk of await generateChunks()) {
        controller.enqueue(encoder.encode(chunk));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Cache-Control": "no-store", // ストリームはキャッシュしない
    },
  });
}
```

> **Edge ランタイムの制約**：Edge で動かす場合、**25秒以内にレスポンス送信を開始**しないとストリーミング能力を失い、その後は**最大300秒**までストリームできます（[limits](https://vercel.com/docs/functions/limitations)）。AI のチャットUIは [Vercel AI SDK](/blog/vercel-ai-sdk-production-llm-apps-streaming-tools-rag) を使うと、このストリーミングを型安全に扱えます。

---

## waitUntil：レスポンス後のバックグラウンド処理

「ユーザーには即返したいが、ログ・分析・Webhook 転送・キャッシュ更新は確実にやりたい」——この定番要件が `waitUntil` です。レスポンスを返した**後**もインスタンスを生かして後処理を完走させます。

```ts
import { waitUntil } from "@vercel/functions";

export async function POST(request: Request) {
  const event = await request.json();

  const result = await processOrder(event); // ユーザーが待つ処理

  // レスポンスは即返す。後処理はバックグラウンドで継続
  waitUntil(
    Promise.allSettled([
      logToAnalytics(event),          // 分析
      sendSlackNotification(result),  // 通知
      revalidateRelatedCaches(result) // キャッシュ更新
    ])
  );

  return Response.json({ ok: true, orderId: result.id });
}
```

> **冪等性とセットで**：`waitUntil` の後処理や Webhook 受信は「少なくとも1回（at-least-once）」を前提に**冪等**に設計します。同じイベントが二重に届いても、二重通知・二重課金が起きないように。冪等キーの設計は [決済の冪等性ガイド](/blog/stripe-payments-production-guide-webhooks-idempotency-subscriptions) と同じ原則です。`Promise.allSettled` を使い、1つの後処理失敗が他を巻き込まないようにしているのもポイントです。

---

## Cron Jobs：スケジュール実行を安全に

バックアップ、通知、サブスク数量更新——定期実行は Cron で。Vercel は本番デプロイURLに対し**HTTP GET** を投げて起動します（[Cron Jobs](https://vercel.com/docs/cron-jobs)）。

### 定義

```json
// vercel.json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "crons": [
    { "path": "/api/cron/cleanup", "schedule": "0 0 * * *" },
    { "path": "/api/cron/digest",  "schedule": "0 9 * * 1" }
  ]
}
```

cron 式の注意点：**タイムゾーンは常に UTC**。`MON`/`JAN` のような別名は非対応。**「日（DoM）」と「曜日（DoW）」を同時指定できない**（片方を `*` に）。

### 必ず保護する（CRON_SECRET）

Cron のパスは公開URLです。**誰でも叩ける**ので、`CRON_SECRET` で認可します。Vercel は Cron 起動時に `Authorization: Bearer <CRON_SECRET>` を付与します（環境変数に `CRON_SECRET` を設定した場合）。

```ts
// app/api/cron/cleanup/route.ts
export async function GET(request: Request) {
  // ① 秘密トークンで認可（外部からの不正起動を弾く）
  const auth = request.headers.get("authorization");
  if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response("Unauthorized", { status: 401 });
  }

  // ② 複数の Cron が同じパスを共有する場合、どのスケジュールかを判別
  const schedule = request.headers.get("x-vercel-cron-schedule"); // 例: "0 0 * * *"

  // ③ 冪等に：同じ時刻に二重起動しても安全な処理
  const deleted = await deleteExpiredSessions();

  return Response.json({ ok: true, schedule, deleted });
}
```

```ts
// 環境変数の生成（ローカル）
// openssl rand -hex 32 で生成し、vercel env add CRON_SECRET production で登録
```

Cron 起動のリクエストは `User-Agent: vercel-cron/1.0` を持つので、必要なら併せて検証できます。

> **重い Cron は分離する**：Cron 関数も `maxDuration` の制約を受けます。数分を超える一括処理は、Cron では「キックするだけ」にして実体は **[Workflows](https://vercel.com/docs/workflows) / キュー**へ流すのが安全です。

---

## グレースフルシャットダウン

Fluid Compute はスケールインやデプロイ時、インスタンス終了の前にシグナルを送ります。**処理中リクエストの完了・接続のクローズ・バッファのフラッシュ**をハンドリングしておくと、デプロイ時の取りこぼしを防げます。

```ts
// 接続のクリーンアップ例（モジュールスコープで一度だけ登録）
process.on("SIGTERM", async () => {
  await pool.end();        // DB接続プールを閉じる
  await flushTelemetry();  // 計測バッファを送り切る
});
```

ファイルディスクリプタは **1,024（並行実行で共有）** が上限です。接続をリークさせると "too many open files" になります。**接続プールを使い、使い終わったら閉じる**——Fluid の並行性下では特に効きます。

---

## 本番チェックリスト（Functions）

- [ ] モジュールスコープのグローバルに**リクエスト固有データを置いていない**
- [ ] グローバルは DB プール・設定など**リクエスト非依存**のものだけ
- [ ] `maxDuration`・メモリを**用途に合わせて明示**
- [ ] 重い/長い処理は **Workflows・キュー・ジョブへ分離**
- [ ] ストリーミングは `no-store`、Edge なら25秒以内に応答開始
- [ ] 後処理は `waitUntil` ＋ `Promise.allSettled` で**冪等**に
- [ ] Cron は `CRON_SECRET` で**必ず保護**、UTC・冪等・DoM/DoW 排他に注意
- [ ] 接続はプール化し `SIGTERM` でクローズ、FD リークなし

---

## まとめ

Fluid Compute は「サーバーレスの手軽さ」と「サーバーの効率」を両立させますが、その代償として**グローバル状態の共有**という規律を要求します。

1. **リクエスト固有データは関数スコープに閉じる**（最重要・セキュリティ直結）
2. I/O バウンドな処理ほど**並行性とActive CPU課金で安くなる**
3. **`waitUntil` で後処理、ストリーミングで初動を速く**
4. **Cron は CRON_SECRET で必ず守り、冪等に**
5. 長時間処理は **Workflows へ分離**

次は、返すレスポンスを速くする [キャッシュ・ISR・Cache Components ガイド](/blog/vercel-caching-isr-cache-components-ppr-guide) へ。

> 本記事は [Vercel Functions](https://vercel.com/docs/functions) / [Fluid Compute](https://vercel.com/docs/fluid-compute) / [Cron Jobs](https://vercel.com/docs/cron-jobs) 公式ドキュメント（2026年6月時点）に基づきます。仕様・上限値は更新されるため、本番採用時は公式で最新値を確認してください。
