プロトコル

GraphQLサブスクリプションとは?graphql-wsでの実装とスケール設計を解説

GraphQLサブスクリプションは、QueryとMutationに並ぶ3つ目のルート操作で、サーバー側で起きたイベントをクライアントへ流し続ける仕組みです。1回のリクエストに1回のレスポンスを返す他の2つと違い、購読が続くあいだ結果が何度も届きます。仕様が定めているのは実行モデルまでで、どのプロトコルで運ぶかは仕様の外。ここが実装判断の分かれ目です。

まとめ:GraphQLサブスクリプションを採用する条件と見送る条件

採用してよいのは、受け取る側が能動的に画面を見続けていて、更新間隔が数秒から数十秒と短く、既にGraphQLでAPIを提供している場合です。チャット、共同編集、進捗表示、取引板のような画面が該当します。ここでポーリングを選ぶと更新の無い時間帯のリクエストが丸ごと無駄になり、間隔を詰めるほど負荷が接続維持より重くなる。この逆転が採用の根拠です。

見送る条件も明確にしておきます。更新が1分に1回も起きない、同時接続が数十で頭打ち、通知だけ届けばデータ本体は後から取ればいい。どれかに当てはまるなら、ポーリングかServer-Sent Eventsで足ります。購読を1本入れると、PubSubの分散、接続数に比例するメモリ、アイドルタイムアウト、再接続時の取りこぼしが全部ついてきます。

QueryやMutationと違うSubscriptionの実行モデルと構成要素

まず実行モデルを押さえます。ここが曖昧なままだと、動くけれど本番で壊れます。

Subscriptionが返すのは値ではなくイベントのストリーム

QueryとMutationのリゾルバは値を返して終わりです。Subscriptionのリゾルバが返すのは、値ではなくイベントの供給源。購読が始まると接続が張られたままになり、供給源にイベントが流れるたびに実行結果が1件ずつ送られます。

制約が1つあります。1回のSubscription操作で指定できる根フィールドはちょうど1つで、Queryのように複数を並べることはできません。複数種類のイベントを1画面で受けるなら、購読を複数本張るか、種別を持つユニオン型を1フィールドで返す設計になります。

subscribeとresolveの2段構成で書くサーバー側の実装

サーバー側のフィールド定義は2つの関数で構成します。subscribeはイベントの供給源となる非同期イテレータを返す関数で、resolveは流れてきたペイロードを戻り値へ変換する関数。省くとペイロードがそのまま値になります。

この2段構成が効くのは、供給源のペイロードを軽くしたいときです。イベントには識別子だけを載せ、resolve側でデータベースから本体を引く。PubSubを流れるメッセージが小さくなり、認可判定も1か所へ集約できます。イベントの粒度と型の対応は、GraphQLスキーマ設計の核心原則の考え方が当てはまります。

仕様September 2025版が整理したストリームの二層構造

GraphQL仕様のリリース履歴をGitHub上で2026年8月8日に確認したところ、正式版は2015年7月、2018年6月、2021年10月、2025年9月の4本で、最新は2025年9月版でした。

この版で購読まわりが整理され、イベントの発生源を表すソースイベントストリームと、実行して得たレスポンスストリームの二層で説明されるようになりました。開始時に引数が不正なら購読は成立せずエラーを返し、成立後の処理で落ちた場合はその1件だけがエラー付きで流れ、購読は続く。この違いを反映しないと、1件の失敗で画面全体をエラー表示にしてしまいます。

WebSocketとSSEとマネージドから選ぶトランスポートの決め方

どのプロトコルで運ぶかは実装側の裁量です。選択を誤ると、後から差し替えるコストが大きくなります。

graphql-wsとsubscriptions-transport-wsの名前の罠

WebSocketで運ぶ場合、ライブラリ名とサブプロトコル名が交差していて紛らわしい状態にあります。現行のgraphql-wsライブラリが実装するサブプロトコル名はgraphql-transport-wsで、旧世代のsubscriptions-transport-wsライブラリが使うサブプロトコル名がgraphql-wsです。

実害はハンドシェイクで出ます。接続要求にはSec-WebSocket-Protocolヘッダでサブプロトコル名が入り、サーバーが受け付けない名前なら接続が成立しない。「ライブラリを新しくしたら繋がらなくなった」の多くはこれで、ヘッダを見れば切り分けは1分です。旧ライブラリは保守が止まっているとApollo公式も案内しています。

graphql-sseでHTTPのまま配信する構成が向く場面

WebSocketを張らずHTTPのまま流す道もあります。The Guildのgraphql-sseがそれで、2026年8月8日に確認した最新は2.6系(2025年10月公開)。プロトコル文書では、操作ごとに個別のSSE接続を張る方式と、1本の接続へ全ての結果を流す方式が定義されています。

後者には予約という手順が入ります。クライアントがPUTで接続を予約し、サーバーが201とトークンを返し、そのトークンを添えてSSE接続と各操作を送る。操作が受理されると202が返り、結果は予約済みの接続へ流れます。EventSourceがカスタムヘッダを送れない制約を、予約時の通常リクエストで回避する形です。方式の比較はSSEとWebSocketの違いと使い分けに整理してあります。

方式 サブプロトコル・規約 双方向 向く場面
graphql-ws graphql-transport-ws 双方向 購読が多く低遅延が要る画面
旧ライブラリ(保守停止) graphql-ws(旧名) 双方向 既存資産の保守のみ
graphql-sse GraphQL over SSE 受信のみ HTTP経路をそのまま通す構成
マネージド 提供元の実装に従う 提供元による 接続維持を自前で持たない構成

AppSyncのようなマネージドへ寄せる判断と自前運用との境界

接続の維持を外部へ預ける選択もあります。AWS AppSyncはGraphQL APIをマネージドで提供し、購読の接続管理とスケールをサービス側が受け持つ構成。詳細はAWS AppSyncとは?GraphQL APIの仕組みと料金にまとめました。

境界の引き方はこうです。要件が「更新があったら通知が届く」程度で既にAWS上に構築しているなら、マネージドへ寄せたほうが運用の総量は減ります。購読のたびに独自の認可判定を挟む、既存サーバーにリゾルバ資産が積み上がっているなら自前運用が素直。GraphQLフェデレーションとは?サブグラフ合成とルーターの実装で扱ったとおり、合成された構成では購読の扱いが単純になりません。

graphql-ws 6系でサーバーとクライアントを実装する手順

ここからは自前実装の具体です。一次情報はGitHub上のリリースノートとプロトコル文書を参照しました。

graphql-ws v6で入った破壊的変更とv5からの移行手順

リリース一覧を確認した時点でgraphql-wsの最新は6.2系(6.2.1・2026年8月6日公開)、6.0.0は2025年1月15日公開でした。5系からの移行では破壊的変更が複数入っています。

  • インポートパスから/lib/が消えた。graphql-ws/use/wsのように書き換える
  • 実行環境の下限が上がった。Node.jsはv20以上、ピア依存のgraphqlは15.10.1以上または16系
  • wsのv7サポートと、非推奨だったfastify-websocketのサポートが落ちた
  • 型が整形済みのものに変わった。onErrorGraphQLFormattedErroronNextFormattedExecutionResult
  • 非推奨APIが削除された。isMessagevalidateMessageへ、isFatalConnectionProblemshouldRetry
  • 各フックが受け取る引数が縮んだ。メッセージ全体ではなくIDと関連部分だけが渡る

加えて@fastify/websocketでは、コンテキストのextra.connectionextra.socketへ改名されました。graphql-ws/clientから読み込めばgraphqlへの依存を持たない構成にもできます。

メッセージ型8種類と切断時に返るクローズコードの読み方と対処法

プロトコル文書で定義されているメッセージ型は8つです。接続開始のConnectionInit、応答のConnectionAck、生存確認のPingPong、購読を始めるSubscribe、結果を運ぶNext、失敗を伝えるError、終了のComplete。通信ログのどこで止まっているかが即座に分かります。切断の理由はクローズコードが語ります。

コード 意味 典型的な原因
4400 Bad Request 仕様外の形式のメッセージを送った
4401 Unauthorized ConnectionAck前に操作を送った
4403 Forbidden 認証で接続を拒否した
4408 初期化のタイムアウト 初期化メッセージが来ない
4409 購読IDの重複 同じ操作IDで購読が二重に来た
4429 初期化要求が多すぎる 初期化を複数回送った

現場で頻度が高いのは4401と4409です。4401は接続直後に購読を投げる実装で起きるので、ConnectionAckを待ってからSubscribeを送る順序を守れば消えます。4409は操作IDの生成が甘いときで、再接続時に同じIDを再利用する実装で踏みます。

Apollo Server 5系でWebSocketサーバーを併設する構成

Apollo Serverでは購読は追加作業になります。公式ドキュメントはフェデレーションを使わない構成では購読の組み込み対応が無いと明記しており、外部ライブラリの併用が前提。リリース一覧では2026年8月8日時点の最新が5.5系(2026年5月公開)でした。

制約が1つあります。startStandaloneServerでは購読を扱えないため、expressMiddlewareへ切り替えてHTTPサーバーを自分で持ち、同じサーバーにWebSocketサーバーを載せます。骨格は次のとおり。

const httpServer = createServer(app);
const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });
const serverCleanup = useServer({ schema }, wsServer);

忘れやすいのが停止処理です。デプロイのたびにプロセスは落ちますが、WebSocket側を明示的に閉じないと接続が宙に浮きます。useServerが返す後片付け用のオブジェクトをApollo Serverのプラグインから呼び、HTTPの排出とWebSocketの排出を両方つないでおいてください。

本番で壊れる箇所はPubSubと認証と再接続の3か所に集まる

本番で動かない原因は、ほぼこの3か所に集まります。

インメモリのPubSubが複数インスタンスで届かなくなる理由

graphql-subscriptionsPubSubクラスは配送をプロセス内のメモリで行うため、サーバーを複数インスタンスに増やした瞬間に破綻します。インスタンスAで発行されたイベントは、インスタンスBに接続している購読者には届かない。Apollo公式ドキュメントも本番向けではないと明示しています。

症状は「自分の更新は見えるが他人の更新が見えない」「たまに届く」という形で出ます。ロードバランサがどのインスタンスへ振ったかで結果が変わるためです。対処は配送をプロセス外へ出すこと。RedisやKafkaを後ろに置いたPubSubEngineの実装へ差し替えます。検証環境が1台だと表面化しません。

connection_initで認証しトークン失効まで扱う実装の型

WebSocketのハンドシェイクにはカスタムヘッダを付けられません。ブラウザのAPIに認証ヘッダを差し込む口が無いためです。そこでconnection_initのペイロードにトークンを載せ、サーバー側のonConnectで検証する形が定石になります。接続の前提そのものはWebSocketとは?仕組み・ハンドシェイクからNode.js実装までに整理してあります。

見落とされがちなのがトークンの寿命です。購読は数時間つながり続けることがあり、その間にアクセストークンは失効します。手は2つで、有効期限を保持して期限到来時に接続を閉じ再接続させるか、配信のたびに認可を再判定するか。前者は実装が軽く、後者は権限剥奪への追従が速い。権限変更が業務に直結する画面なら後者です。

再接続時に起きる重複配信と欠損を前提にした冪等化と補完の設計

接続はいつか切れます。電波の切り替わり、端末のスリープ、デプロイ。クライアントは自動で再接続しますが、切れていた数秒から数十秒に発行されたイベントは失われます。graphql-transport-wsには、切断前の続きから再開する仕組みが規定されていないからです。

設計は2つの組み合わせです。1つ目は重複への耐性で、イベントに一意なIDを持たせて処理済みかを見る。2つ目は欠損の補完で、再接続の直後に「最後に受け取った時刻より後の差分」をQueryで取り直します。購読は通知路、Queryは正の取得路と役割を分ければ、取りこぼしがあっても画面は正しい状態へ収束する。なおSSEにはLast-Event-IDによる再開の仕組みがあります。

接続を維持し続ける運用でLBとタイムアウトと監視をどう設計するか

購読は接続を張り続ける構成なので、前段の設定と噛み合わないと切断が多発します。

ロードバランサのアイドルタイムアウトとping/pongの間隔

最初に確認するのは経路上のアイドルタイムアウトです。ALBの既定値は60秒、nginxのproxy_read_timeoutの既定値も60秒。購読は更新が無ければ何も流れないので、静かな時間帯が60秒続いた時点で無通信とみなされて切られます。夜間だけ切断が増える症状はたいていこれです。

graphql-wsのサーバー側にはこれを埋める仕組みが入っています。useServerkeepAliveオプションで、ソースコード上の既定値は12000ミリ秒でした。この間隔でWebSocketレベルのpingを送り、pongが返らなければ接続を破棄する。12秒ごとに通信が発生するため、60秒のアイドルタイムアウトには引っかかりません。経路上で最も短い値より短くする、それだけです。

同時接続数がそのままコストになる構成とスケールイン時の強制切断

リクエスト単位の課金感覚で見積もると外します。接続が生きているあいだメモリとファイルディスクリプタが占有され続け、同時接続数がそのまま必要なリソース量になる。1接続あたりが小さくても、1万接続なら固定費として効いてきます。

スケールインも設計に入れてください。インスタンスが減るとその接続は切れ、クライアントは一斉に再接続し、前節の差分取得Queryも同時に集中します。散らす手立ては2つで、再接続待ち時間に指数的な後退とゆらぎを入れること、縮小をゆるやかにして接続の排出に猶予を持たせること。デプロイも同じ理屈です。

配信遅延と接続断を切り分けるために監視へ載せておく指標の一覧と閾値

障害報告は「更新が来ない」という形で来ます。配信の遅延か接続の断絶かを分けられないと調査が長引く。最低限これは取ってください。

  1. 同時接続数と購読数。接続あたりの購読本数が想定より多いなら、画面側で購読を張りすぎている
  2. イベント発行から配信までの所要時間。PubSubの詰まりはここに出る
  3. クローズコード別の切断件数。4401や4408が増えていればクライアント実装、4409ならID生成の問題
  4. 再接続の発生率。特定インスタンスに偏るなら、そのノードかその前段の設定

閾値は絶対値ではなく変化率で見ます。切断件数の平常時からの倍率、配信遅延のp95の推移。購読は正常時でも切断が起き続けるため、ゼロを目標にすると警報が鳴りやみません。

受託開発でサブスクリプションを見送る条件と代替になる設計の順序

技術として成立することと、採るべきかは別に切り分けます。

更新頻度と同時接続数から採用の可否を決めるときの実務的な閾値の置き方

判断の軸は2つで足ります。更新の頻度と、同時に画面を開いている人数です。更新が1分に1回も起きない画面なら、30秒間隔のポーリングで体感はほとんど変わりません。同時接続が数十で頭打ちなら、ポーリングの負荷は接続維持の運用コストを下回ります。

逆に、更新が数秒おきに起きる、同時接続が数百から数千に届く、更新の遅れが業務の判断を狂わせる。これらが揃うなら購読の出番です。見積もりには、PubSubの基盤、接続維持ぶんのインスタンス、再接続と冪等化の実装、監視の追加まで入れてください。購読の実装自体は半日で終わることもありますが、周辺は数日から数週間かかります。

ポーリングとSSEとサブスクリプションを試す順序と切り替え条件

迷ったら軽いほうから試します。最初はポーリングで、GraphQLクライアントの多くはQueryに間隔を指定するだけで動く。次にServer-Sent Eventsで、受信だけでよければHTTPの経路そのままに一方向の配信ができます。

切り替えの判断材料も決めておきます。ポーリングからSSEへ移るのは、更新の遅れが体感で問題になったときか、空振りのリクエストが負荷として無視できなくなったとき。SSEから購読へ移るのは、双方向のやり取りが要るときです。前提となるGraphQL自体の考え方はGraphQLとは?REST APIとの違い・メリット・デメリットにまとめてあります。

スキーマ設計から運用設計まで含めて外部に相談するときの進め方

購読を入れる案件でつまずくのは、コードよりも設計の順番です。イベントの粒度、購読の単位、認可をどの層で判定するか。画面から必要なイベントを洗い出し、スキーマのSubscription型を決め、配送基盤を選び、最後にトランスポートを決める。この向きで進めると手戻りが出ません。

既存APIの棚卸しからイベント設計、配送基盤の選定、接続を維持する構成の運用設計までをまとめて相談したい場合は、API開発・システム連携で扱っている領域です。RESTで動いている資産があるなら、購読部分だけを段階的に載せる進め方も取れます。

よくある質問

導入検討でよく挙がる質問を、実装と運用の観点でまとめました。

subscriptions-transport-wsからgraphql-wsへ移行する必要はありますか?

新規に書くなら最初からgraphql-wsを選んでください。旧ライブラリは保守が止まっているため、依存の脆弱性対応や新しいNode.jsへの追従で行き詰まります。段階移行が要るなら、Sec-WebSocket-Protocolの値で両方のサブプロトコルを振り分ける構成も取れる。旧側の接続がゼロになってから受け口を閉じます。

WebSocketとSSEはどちらを選ぶべきですか?

クライアントから送るものが購読の開始と停止だけなら、SSEで足ります。HTTPのままなので経路上のプロキシやCDNで詰まる可能性が下がり、切断からの再開も組みやすい。双方向にやり取りしたい、購読の本数が多くて接続を1本にまとめたいならWebSocketです。迷う場合は、既存インフラでWebSocketのアップグレードが通るかを先に確認してください。

Subscriptionで大きなデータを丸ごと送っても問題ありませんか?

避けたほうが無難です。購読者が増えるほど同じデータが人数分だけ送られ、帯域とメモリを圧迫します。イベントには識別子と更新種別だけを載せ、受け取った側が必要に応じてQueryで本体を取る形にすると安定する。購読者ごとに見せてよい範囲が違うケースでも、Query側に認可判定を集約できます。

フェデレーション構成でサブスクリプションは使えますか?

使えますが、QueryやMutationほど単純ではありません。ルーターが購読を終端してサブグラフとの間で結果を中継する形になるため、実装とライセンス条件の両方を確認してください。落としどころとしては、購読を提供するサブグラフを1つに絞る分担が扱いやすいはずです。

購読が増えるとサーバーは何台くらい要りますか?

接続数と更新頻度の両方から見積もります。接続の維持はメモリとファイルディスクリプタを、配信はCPUと帯域を消費する。前者は同時接続数に、後者は「イベント数×購読者数」に比例します。多数の購読者が同じイベントを受け取る構成では、接続数が同じでも配信の負荷だけが跳ね上がる。想定値で負荷試験を1回かければ、台数もPubSubの詰まり方も分かります。

関連記事

資料請求

RELATED POSTS 関連記事