Vercel Functions は「api/ にファイルを置けば動く」ところまでは簡単です。難しいのは本番品質——並行リクエストでデータが混ざらないか、I/O 待ちでコストが膨らまないか、後処理を取りこぼさないか、Cron が無防備に叩かれないか。この記事は、Vercel Functions と Fluid Compute の公式仕様に忠実に、落ちない・追える・安い関数の作り方を実コードでまとめます。
全体像(コンピュート以外のレイヤー)は Vercel 本番運用ガイド を、課金の詳細は Active CPU 最適化ガイド を参照してください。本稿は「Functions をどう書くか」に集中します。
Fluid Compute:1インスタンスで複数リクエスト
なぜ速く・安くなるのか
従来のサーバーレスは「1リクエスト=1インスタンス(microVM)」でした。これだと、
- リクエストのたびにコールドスタートが起こりうる
- 関数が DB や AI の応答を待っている間もインスタンスが1リクエストを占有し、遊ぶ
Fluid Compute は、1つの関数インスタンスが複数の呼び出しを並行処理します。公式の言葉では「最適化された並行性(optimized concurrency)」。I/O 待ちの間に同じインスタンスが別のリクエストを処理できるため、コールドスタートが減り、必要なインスタンス総数が減り、コストが下がる。AI(埋め込み・ベクトル検索・外部API)のような I/O バウンドな処理で特に効きます。
// 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 の本質は 「複数リクエストが同一プロセス=グローバル状態を共有する」 ことです。これは性能上の利点であると同時に、最も多いセキュリティバグの原因になります。
// ❌ 危険:リクエスト固有のデータをモジュールスコープに置く
let currentUser: User | null = null; // 全リクエストで共有される!
export async function GET(request: Request) {
currentUser = await authenticate(request); // 別リクエストが上書きする競合
return Response.json(await getDashboard(currentUser)); // 他人のデータが混ざりうる
}
// ✅ 安全:リクエスト固有のデータは関数スコープに閉じる
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)。
| ランタイム | 最適化された並行性 | 使いどころ |
|---|---|---|
| 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秒はベータ(関数単位設定)。既定の放置は避け、用途に合わせて明示します。
// 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 で実装できます。
// 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)。AI のチャットUIは Vercel AI SDK を使うと、このストリーミングを型安全に扱えます。
waitUntil:レスポンス後のバックグラウンド処理
「ユーザーには即返したいが、ログ・分析・Webhook 転送・キャッシュ更新は確実にやりたい」——この定番要件が waitUntil です。レスポンスを返した後もインスタンスを生かして後処理を完走させます。
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)」を前提に冪等に設計します。同じイベントが二重に届いても、二重通知・二重課金が起きないように。冪等キーの設計は 決済の冪等性ガイド と同じ原則です。Promise.allSettledを使い、1つの後処理失敗が他を巻き込まないようにしているのもポイントです。
Cron Jobs:スケジュール実行を安全に
バックアップ、通知、サブスク数量更新——定期実行は Cron で。Vercel は本番デプロイURLに対しHTTP GET を投げて起動します(Cron Jobs)。
定義
// 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 を設定した場合)。
// 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 });
}
// 環境変数の生成(ローカル)
// openssl rand -hex 32 で生成し、vercel env add CRON_SECRET production で登録
Cron 起動のリクエストは User-Agent: vercel-cron/1.0 を持つので、必要なら併せて検証できます。
重い Cron は分離する:Cron 関数も
maxDurationの制約を受けます。数分を超える一括処理は、Cron では「キックするだけ」にして実体は Workflows / キューへ流すのが安全です。
グレースフルシャットダウン
Fluid Compute はスケールインやデプロイ時、インスタンス終了の前にシグナルを送ります。処理中リクエストの完了・接続のクローズ・バッファのフラッシュをハンドリングしておくと、デプロイ時の取りこぼしを防げます。
// 接続のクリーンアップ例(モジュールスコープで一度だけ登録)
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 は「サーバーレスの手軽さ」と「サーバーの効率」を両立させますが、その代償としてグローバル状態の共有という規律を要求します。
- リクエスト固有データは関数スコープに閉じる(最重要・セキュリティ直結)
- I/O バウンドな処理ほど並行性とActive CPU課金で安くなる
waitUntilで後処理、ストリーミングで初動を速く- Cron は CRON_SECRET で必ず守り、冪等に
- 長時間処理は Workflows へ分離
次は、返すレスポンスを速くする キャッシュ・ISR・Cache Components ガイド へ。
本記事は Vercel Functions / Fluid Compute / Cron Jobs 公式ドキュメント(2026年6月時点)に基づきます。仕様・上限値は更新されるため、本番採用時は公式で最新値を確認してください。