
Vercel Functions が WebSocket に対応したので試してみた
はじめに
Vercel は 2026 年 6 月 22 日、WebSocket 接続をネイティブに受け付ける機能をパブリックベータとして発表しました。
ここでいう「ネイティブ」とは、Function の最大実行時間の範囲内で WebSocket の Upgrade リクエストを受け付け、1 つの接続を 1 つの Function インスタンスに固定して処理できるというものです。過去記事 では Vercel が WebSocket をサポートしないと紹介しました。本記事では、ここがどう変わったのか、実際に Vercel へ WebSocket サーバーをデプロイして確かめます。
具体的には、公式ドキュメントが説明する「双方向通信ができる」「接続は既定の Function 実行時間 (5 分) で切れる」という 2 点を、接続して確認します。実際のところ、接続は約 5 分 10 秒で切断され、切断のされ方は正常な終了ではなく、通信異常として検出される形でした。 ただし再接続はすぐに成立します。
Vercel Functions の WebSocket 対応とは
WebSocket 接続は、HTTP の GET リクエストに Upgrade ヘッダーが付いた形で始まります。Vercel はこれを、Routing Middleware や rewrites、Firewall ルールなど、通常のリクエストと同じ経路で処理したうえで WebSocket に昇格させます。昇格した接続は、その接続が続く間ずっと同じ Function インスタンスに固定されます。Fluid Compute の仕組みにより、1 つのインスタンスが複数の WebSocket 接続を同時に処理できます。
対応フレームワークは幅広く、プレーンな Node.js + ws、Express・Hono・h3 などのサーバーフレームワーク、Socket.IO、Bun ランタイムのネイティブ Bun.serve()、Nitro、Python が挙げられています。
検証環境
- Node.js ランタイム: v24.18.0
- 検証用ライブラリ: サーバー・クライアントとも ws
対象読者
- Vercel で WebSocket を使ったリアルタイム機能を検討している方
- 「Vercel は WebSocket に非対応」という理由で、これまで外部のリアルタイム基盤を選んでいた方
- 接続が何秒でどのように切れるのか、実測のデータを知りたい方
参考
- WebSockets (Vercel 公式ドキュメント)
- Vercel Functions Limits (公式ドキュメント)
- Does vercel support websockets now that we have fluid compute? (Vercel コミュニティ、2025-11-12)
検証方法
最小構成のエコーサーバーを Vercel にデプロイしました。api/ws.js から素の http.createServer と WebSocketServer をエクスポートするだけの構成です。
受信した JSON メッセージは、そのまま送り返します。接続開始からの経過時間は、サーバー内の単調時計 process.hrtime.bigint() で計測します。open・message・close・error の発生ごとに console.log へ記録します。
ログは vercel logs --json で取得できます。
const wss = new WebSocketServer({ server });
wss.on('connection', (ws) => {
const startedAt = process.hrtime.bigint();
const elapsedMs = () => Number(process.hrtime.bigint() - startedAt) / 1e6;
ws.on('close', (code, reason) => {
log({ event: 'close', elapsedMs: elapsedMs(), code, reason: reason.toString() });
});
});
クライアントも同様に、performance.now() による単調時計で経過時間を計測します。壁時計をホスト間で突き合わせる方法は使いませんでした。NTP 同期のずれや、ログ取り込みの遅延の影響を受けるためです。
クライアントは次の 3 つを順に実施します。
- 接続してメッセージを 1 往復させ、基本的な疎通を確認する
- 接続を保持し、20 秒おきに Ping 制御フレーム
ws.ping()を送って Pong の受信を記録する。安全装置として、最大 400 秒で接続を閉じる - 2 の接続が終了したら、指数バックオフで再接続を試み、エコーメッセージが通ることを確認する
pingTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 20000);
ws.on('close', (code, reason) => {
finish({ reason: 'closed', code, closeReason: reason.toString(), elapsedMs: elapsed() });
});
検証結果: 基本的な双方向通信
エコーサーバーへ接続し、メッセージを送信してから応答が返るまでを確認しました。
接続・送信・応答は、すべて問題なく成立しました。サーバー側のログにも、同じやり取りが記録されていました。
{"event":"echo_verify_message","elapsedMs":752,"data":"{\"type\":\"echo\",...,\"serverElapsedMs\":227.6}"}
双方向のメッセージ送受信は、公式ドキュメントの記載どおり動作しました。
検証結果: 接続時間の上限と再接続
接続を保持したまま Ping を送り続け、何秒で・どのような形で接続が終了するかを 2 回試行しました。
結果は次のとおりです。
| 試行 | 切断までの経過時間 [s] | クローズコード | 再接続 |
|---|---|---|---|
| 1 | 310.4 | 1006 (異常終了、Close フレームなし) | 1 回目の試行 (1 秒待機後) で成功 |
| 2 | 310.9 | 1006 (異常終了、Close フレームなし) | 1 回目の試行 (1 秒待機後) で成功 |
Function の maxDuration は 300 秒でしたが、実際に切れたのは 310 秒前後でした。2 回とも 10 秒ほどの差があり、単発の誤差ではなく再現する挙動だとわかります。切断の直前まで、Ping と Pong は正常に往復していました。サーバーは応答できる状態のまま、外側から終了させられたとみられます。
切断はクローズコード 1006 (abnormal closure) として観測されました。これは WebSocket の仕様上、正常な Close フレームを受け取れなかったときにクライアント側が生成する値です。サーバー側のログにも、切断そのものを説明する行 (close イベントや error イベント) は残っていませんでした。アプリケーションに Close フレームを送る猶予を与えず、Function ごと打ち切られている状態だと考えられます。
{"event":"ping_sent","elapsedMs":300260}
{"event":"close","elapsedMs":310431,"code":1006,"reason":""}
再接続は、2 回とも 1 秒待機した最初の試行で即座に成功し、エコーメッセージも正常に返ってきました。接続が切れたあとの復帰自体に問題はありません。
考察
5 分ちょうどではなく 5 分 10 秒前後で切れる、かつ Close フレームを伴わない、という 2 点は、公式ドキュメントだけでは分からなかった事象です。
実装では、close イベントのコードが 1000 番台の正常終了だけを想定していると、コード 1006 を見落とす可能性があります。再接続処理は、コード 1006 のような予期しない切断も含めてトリガーする必要があるでしょう。
まとめ
Vercel Functions は 2026 年 6 月、Fluid Compute 上で WebSocket 接続をネイティブに受け付ける機能をパブリックベータとして追加しました。実際にデプロイして検証したところ、双方向通信は問題なく成立しました。接続は Function の実行時間 (既定 5 分) に応じて終了しますが、実測では 5 分 10 秒前後、クローズコード 1006 (Close フレームを伴わない終了) として観測されました。再接続はすぐに成立します。
かつて「Vercel は WebSocket サーバーに非対応」という理由で外部のリアルタイム基盤を選んでいた場合、単純な双方向接続であれば Vercel だけで完結できる可能性があります。ただし、あくまでパブリックベータなので注意が必要です。本記事の検証結果が、採用を検討する際の判断材料になれば幸いです。






