Socket.IOは、ブラウザとサーバーの間でリアルタイムの双方向通信を行うためのJavaScriptライブラリです。チャット、通知、共同編集、ダッシュボードのように、クライアントの定期取得を待たずにサーバー側の変化を画面へ出したい処理向けで、自動再接続・確認応答・ルーム配信を標準で備えています。
ただしSocket.IOはWebSocketの実装ではなく、素のWebSocketクライアントとは繋がりません。この記事では2026年10月時点の最新版socket.io 4.8.4で動かした結果をもとに、WebSocketとの違い、実装、切断中のイベントの取りこぼし対策、スケール、接続制限までを説明します。
まとめ:Socket.IOの要点
- Socket.IOはWebSocketの実装ではなく、WebSocketなどを下位の通信路に使う独自プロトコルのライブラリです。素のWebSocketクライアントとは相互に接続できません。
- 最新版はサーバー
socket.io・クライアントsocket.io-clientとも4.8.4(2026年9月25日公開)。サーバーの動作要件はNode.js 10.2.0以上です。 - 配信の既定はat most once(最大1回)です。切断中のイベントを受け取るには
connectionStateRecoveryを有効にし、確実に届けたい送信は確認応答とアプリ側の重複排除で補います。 - サーバーを2台以上にするとブロードキャストが分断されます。アダプターでサーバー間をつなぎ、ロングポーリングを使うならスティッキーセッションも必要です。
corsの設定はブラウザ向けのヘッダーで、ブラウザ以外からの接続は止まりません。接続元を制限するならallowRequestでOriginを検査し、接続時のミドルウェアで認証します。
Socket.IOの仕組みと向いているサービス
Socket.IOは接続を張ったままにして、サーバーからも任意のタイミングでデータを送れるようにします。通信はイベント名単位で、送る側がemit()、受ける側がon()を書きます。
Engine.IOによる接続の確立とWebSocketへの昇格
接続の確立と維持は、下位ライブラリのEngine.IOが担います。接続はまずHTTPロングポーリングで始まり、WebSocketが使える環境なら途中でWebSocketへ切り替わります。4.8.4では、connection時点の通信路はpollingで、直後にupgradeイベントが発火してwebsocketに変わりました。WebSocketがプロキシで塞がれていてもロングポーリングのまま動き続けます。
接続の生存確認はハートビートで行います。既定ではサーバーが25秒(pingInterval)ごとにpingを送り、20秒(pingTimeout)以内に応答がなければ切断とみなします(公式のサーバーオプション一覧)。ロードバランサーやリバースプロキシのアイドルタイムアウトは、この合計45秒より長くしておく必要があります。
向いているサービスと向かない用途
向いているのは、ブラウザ相手に「同じ画面を見ている複数人へ同時に変化を届ける」処理です。チャット、入力中表示、在庫の更新通知、管理画面のダッシュボード、共同編集の参加者表示が典型で、ルームでユーザー単位・ドキュメント単位に宛先を絞れます。次の用途では別の手段を選びます。
- モバイルアプリのバックグラウンド通知:公式ドキュメントは、TCP接続を張り続けて電池を消費するため使わず、FCMなどを使うよう明記しています(Socket.IO公式ドキュメント)。
- サーバーから一方向に流すだけの配信:生成AIの逐次出力などはSSEの方が構成が単純です。比較はSSEとWebSocketの違いと使い分けで整理しています。
- 相手が素のWebSocketしか話せない場合:IoT機器や外部サービスのWebSocket APIにはSocket.IOクライアントで接続できません。
- チャット機能そのものを早く出したい場合:既読管理やモデレーションまで自前で作る工数が見合わなければ、チャットSDKも候補です(Sendbirdとは?チャットSDKの料金と実装手順、自前WebSocket実装との使い分け)。
Socket.IOとWebSocketの違い
公式ドキュメントは「Socket.IOはWebSocketの実装ではない」と注意書きを置き、各パケットに独自のメタデータを付けるため、WebSocketクライアントはSocket.IOサーバーに、Socket.IOクライアントはWebSocketサーバーに接続できないと説明しています(What Socket.IO is not)。
4.8.4のサーバーにwsパッケージの素のクライアントで接続すると、ルート(/)宛てはsocket hang upで失敗しました。Engine.IOのパス(/socket.io/?EIO=4&transport=websocket)ならWebSocketは開きますが、届くのは0{"sid":…}という開始パケットで、普通の文字列を送るとサーバーが接続を閉じました。
| 観点 | 素のWebSocket | Socket.IO |
|---|---|---|
| 正体 | 標準プロトコル(RFC 6455) | 独自プロトコルのライブラリ |
| 通信路 | WebSocketのみ | WebSocket・ロングポーリング・WebTransport |
| 自動再接続 | 自前で実装 | 標準搭載 |
| 確認応答 | 自前で実装 | 標準搭載(タイムアウト付き) |
| ルーム・ブロードキャスト | 自前で実装 | 標準搭載 |
| 相互接続 | Socket.IOサーバーに繋がらない | WebSocketサーバーに繋がらない |
ブラウザ相手で、再接続・ルーム・確認応答を自前で書きたくないならSocket.IOです。相手がSocket.IOを話さない、プロトコルを標準のまま公開したいなら素のWebSocketを選びます。WebSocket自体の仕組みはWebSocketとは?仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説、サーバー間通信まで含めた比較はgRPCとWebSocketの違いは?双方向通信の仕組み・ブラウザ到達性・LB通過性で使い分けるで扱っています。
Socket.IOのインストールとバージョン
サーバーはsocket.io、ブラウザやNode.jsのクライアントはsocket.io-clientを入れます。npmレジストリ上の最新は2026年9月25日公開の4.8.4で、package.jsonのenginesはサーバーがnode >=10.2.0、クライアントがnode >=10.0.0です。4.8.4では、確認応答がタイムアウトしたときに保留中の応答を片付けずメモリが増え続ける不具合が直っています([email protected]のリリースノート)。timeout()を多用しているなら更新してください。
# サーバー(後掲のExpress使用例に必要)
npm install [email protected] [email protected]
# クライアント(バンドラを使う場合)
npm install socket.io-client
既定(serveClient: true)ではブラウザ用クライアントが/socket.io/socket.io.jsから配信され、スクリプトタグでも読み込めます。Node.jsの版の選び方はNode.jsとは?仕組み・版の選び方・標準機能で動かす手順を実装目線で解説を参照してください。
サーバーとクライアントの実装
以下のコードはNode.js v26.5.0・socket.io 4.8.4・Express 5.3.0で動作を確認しています。Express自体の書き方はExpress.jsとは?Express 5対応のバックエンド入門と4系からの移行点にまとめています。
サーバー側のセットアップ
const express = require('express');
const { createServer } = require('node:http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: ['https://example.com'] }
});
io.on('connection', (socket) => {
console.log('接続:', socket.id);
socket.on('disconnect', (reason) => {
console.log('切断:', socket.id, reason);
});
});
httpServer.listen(3000);
connectionはクライアントが繋がるたびに発火し、socketがその1接続を表します。socket.idは通常の再接続で別の値になるため、利用者の識別には認証で得たユーザーIDを使います。disconnectのreasonにはtransport closeなどの理由が入ります。
クライアント側の接続
以下はバンドラを使うクライアント側の例です。example.comは接続先の例示で、前掲サーバーをローカルで試す場合は接続先をhttp://localhost:3000にし、サーバーのcors.originにはクライアント画面の実際のオリジンを設定します。
import { io } from 'socket.io-client';
const socket = io('https://example.com');
socket.on('connect', () => {
console.log('接続完了 id=', socket.id);
});
socket.on('connect_error', (err) => {
console.log('接続失敗:', err.message);
});
クライアントは通信障害による切断では既定で自動再接続しますが、明示的な切断では手動の再接続が必要です。また、切断中にemit()したイベントは再接続後に送ります。接続できない原因はconnect_errorのmessageに出ます。
emitとonによるイベントの送受信
イベント名とデータの形は自由に決められ、オブジェクトやバイナリもそのまま送れます。
emitとonによるチャットメッセージの送受信
// サーバー: 受け取ったメッセージを全員へ送り返す
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', { id: socket.id, text: msg });
});
});
// クライアント: 送信と受信
socket.emit('chat message', 'こんにちは');
socket.on('chat message', (payload) => {
console.log(payload.id, payload.text);
});
確認応答(ack)とemitWithAck
相手が受け取ったかを知りたい送信には、最後の引数にコールバックを渡す確認応答を使います。timeout()を挟むと、期限内に応答がなければエラーになります。Promiseで書きたいときはemitWithAck()を使い、タイムアウト時はoperation has timed outというメッセージで例外が投げられました(4.8.4で確認)。
// クライアント: 5秒以内に応答がなければ例外
try {
const res = await socket.timeout(5000).emitWithAck('save', data);
console.log('保存結果:', res.status);
} catch (err) {
console.error('サーバーが期限内に応答しませんでした');
}
// サーバー: 最後の引数のコールバックを呼ぶと応答になる
socket.on('save', async (data, callback) => {
await saveToDb(data);
callback({ status: 'ok' });
});
ブロードキャスト・ルーム(join)・名前空間
複数のクライアントへの配信は宛先の広さで書き分けます。取り違えると、自分の発言が二重に表示される不具合になります。
emit・broadcast・toによる配信先の違い
| 書き方 | 届く相手 |
|---|---|
socket.emit() |
その接続だけ |
io.emit() |
接続中の全員(送信者を含む) |
socket.broadcast.emit() |
送信者以外の全員 |
io.to('room').emit() |
そのルームの全員 |
socket.to('room').emit() |
そのルームの送信者以外 |
io.to(socketId).emit() |
特定の1接続 |
ルームへのjoinとleave
ルームはサーバー側だけの概念です。各接続は自分のsocket.idと同名のルームに自動で入っています。ユーザーIDのルームに入れておけば、同じ人が複数タブで開いていても全タブへ届きます。
io.on('connection', (socket) => {
const userId = socket.data.userId; // 認証ミドルウェアで設定した値
socket.join(`user:${userId}`); // 個人宛ての通知用
socket.on('enter room', (roomId) => {
// 非公開ルームでは、認証済みユーザーの参加権限を検証してからjoinする
socket.join(`room:${roomId}`);
socket.to(`room:${roomId}`).emit('joined', userId);
});
socket.on('leave room', (roomId) => socket.leave(`room:${roomId}`));
});
切断した接続はすべてのルームから自動で抜けます。ルームに入っていないクライアントにはto(room)のイベントは届きません。
名前空間によるアプリと認証の分離
名前空間は1本の接続の上でアプリを論理的に分ける単位で、管理画面と一般利用者で認証ミドルウェアを分けたいときに使います。
const admin = io.of('/admin');
admin.use(requireAdmin); // 管理者だけ通すミドルウェア
admin.on('connection', (socket) => {
socket.emit('metrics', getMetrics());
});
切断中のイベントを取りこぼさない設計
モバイル回線のように切断と再接続が多い環境では、既定の配信保証のままだと通知やメッセージの欠落が起きます。
配信保証の既定値はat most once
公式のDelivery guaranteesによると、Socket.IOはメッセージの順序は通信路が切り替わる間も含めて保証しますが、到達については既定でat most once(最大1回)です。送信中に接続が切れたイベントは相手に届いた保証がなく、再接続後に再送もされません。サーバーから切断中のクライアントへ送ったイベントは失われます(実測でも、復元を有効にしていないサーバーでは受信0件でした)。
Connection State Recoveryによる切断中イベントの復元
サーバーから見た取りこぼしは、connectionStateRecovery(4.6.0以降)で埋められます。予期しない切断のあと、サーバーは接続のid・参加ルーム・socket.dataと、その間に送られたパケットを保持し、期限内に戻ってきたクライアントへ状態を戻して未送信分を届けます(Connection state recovery)。
const io = new Server(httpServer, {
connectionStateRecovery: {
maxDisconnectionDuration: 2 * 60 * 1000, // 保持期間(既定2分)
skipMiddlewares: false // 復元時も認証ミドルウェアを通す
}
});
io.on('connection', (socket) => {
if (socket.recovered) {
// id・rooms・dataが戻り、切断中のイベントは届け済み
} else {
// 新規か復元できなかった接続:ルーム参加と初期データ送信をやり直す
}
});
4.8.4で、ルーム参加後に通信を切り、切断中にそのルームへemitしてから再接続させると、socket.recoveredがtrue、socket.idは切断前と同じで、切断中のイベントも届きました。注意点が2つあります。
- 空の設定
{}で有効にするとskipMiddlewaresはtrueでした。復元した接続は認証ミドルウェアを通らず、切断中に停止したアカウントも戻れます。毎回認証するならfalseを明示します。 - 切断前に一度もサーバーからイベントを受け取っていない接続は復元されませんでした。復元は最後に受け取ったパケットの位置を手がかりにするため、接続直後に初期データを1件送っておくと対象に入ります。
この機能はアダプターの対応が必要で、公式の対応表ではメモリ上の標準アダプター・Redis Streams・MongoDBが対応、Redis(Pub/Sub)アダプターは非対応です。
retriesで再送するときの重複対策
クライアントからサーバーへの送信は、retriesとackTimeoutを指定すると、確認応答が返るまで最大retries + 1回送り直すat least once(最低1回)になります。ただし重複して届くことがあります。ackTimeout: 300, retries: 3のクライアントから、3回目まで応答しないサーバーへ1回emitすると、サーバーのハンドラーは同じデータで3回呼ばれました。
const socket = io('https://example.com', { ackTimeout: 10000, retries: 3 });
// 送信側で一意のIDを付ける
socket.emit('order', { requestId: crypto.randomUUID(), item: 'A-100' }, (err, res) => {
if (err) console.error('リトライ上限まで応答なし');
});
// サーバー側: request_idにUNIQUE制約を付けたordersへINSERTし、重複キーなら処理済みとして応答
// CREATE TABLE orders (request_id uuid PRIMARY KEY, item text NOT NULL);
socket.on('order', async (order, callback) => {
try {
await db.query('INSERT INTO orders (request_id, item) VALUES ($1, $2)', [order.requestId, order.item]);
callback({ ok: true });
} catch (err) {
if (err.code === '23505') return callback({ ok: true, duplicated: true }); // PostgreSQLの一意制約違反
callback({ ok: false });
}
});
二重処理が許されない操作でretriesを使うなら、一意のIDによる重複排除を必ずセットにします。「処理済みか照会してから作成する」書き方は、再送が処理中に届くと両方が照会を通過するため、上のようにDBの一意制約で弾きます。PostgreSQL 18.6で、応答を遅らせて再送させたところ、ハンドラーは3回呼ばれ、ordersの行は1件でした。タブを再読み込みすると保留中の送信は消えるため、注文や決済はHTTPのAPIで受け、Socket.IOは結果の通知に使う構成の方が安全です。
複数サーバーへのスケール
ルームや接続の情報は各サーバープロセスのメモリにあります。2台並べると、io.emit()は同じサーバーに繋いだ利用者にしか届きません。増設した途端に一部の人へ届かなくなるのはこのためです。
スティッキーセッションが必要な理由
ロングポーリングは1つのセッションで何度もHTTPリクエストを送るため、毎回同じサーバーに届かないとセッションが見つからず失敗します。公式のUsing multiple nodesは、対処としてスティッキーセッション(Cookieや送信元IPで振り分け先を固定)か、クライアントをtransports: ['websocket']にしてロングポーリングを使わない方法を挙げています。後者はWebSocketが塞がれた環境で繋がらなくなります。nginx・HAProxyの設定例も同じページにあります。HAProxy自体はHAProxyとは:L4/L7ロードバランサーの仕組みと設定・nginxとの違いを実装目線で解説を参照してください。
アダプターの選び方
サーバー間でイベントを橋渡しするのがアダプターで、公式チームが保守するものにRedis・Redis Streams・MongoDB・Postgres・Cluster・Google Cloud Pub/Sub・AWS SQS・Azure Service Busがあります(Adapter)。版は2026年10月27日にnpmレジストリで確認しました。
| アダプター | 最新版 | 状態の復元 | 向く構成 |
|---|---|---|---|
| @socket.io/redis-adapter | 8.3.0 | 非対応 | Redisが既にある |
| @socket.io/redis-streams-adapter | 0.3.1 | 対応 | Redisの瞬断に強くしたい |
| @socket.io/postgres-adapter | 0.5.0 | 対応作業中 | Postgresだけで運用したい |
| @socket.io/cluster-adapter | 0.3.0 | 非対応 | 1台でNode.jsのclusterを使う |
Redis(Pub/Sub)アダプターの最小構成は次のとおりです。Redis 7.0以上なら、公式は新規開発にシャード化Pub/Subを使うcreateShardedAdapter()を推奨しています(Redis adapter)。
const { createClient } = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
(async () => {
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
})();
Redis Streamsアダプターは、Pub/Sub型と違ってRedisの一時的な切断から取りこぼしなしで再開でき、Valkeyでも動きます。どちらもRedis上のメッセージに署名や暗号化はしないため、Redisを信頼できない網へ公開しないでください。AWSでRedisを用意する選択肢はAWSでRedisを使う方法:ElastiCache・MemoryDB・EC2の選び分けと接続手順で比べています。
本番運用のセキュリティ
corsの適用範囲とallowRequestによる接続制限
Socket.IO v3以降、ブラウザから別オリジンへHTTPロングポーリングで接続させるにはcorsの明示的な設定が必要です(Handling CORS)。これはブラウザに対する許可ヘッダーで、サーバーが接続を拒否する設定ではありません。ブラウザからのWebSocket接続もCORSの対象外です。cors: { origin: ['https://example.com'] }だけを設定したサーバーに、Node.jsのクライアントからOrigin: https://evil.exampleを付けて接続すると、ロングポーリングでもWebSocketでも接続が成立しました。
接続そのものを拒否するには、初回の接続要求を審査するallowRequestでOriginを検査します。同じ条件で、下の設定では両方の通信路でconnect_errorになり、サーバーのconnectionも発火しませんでした。
const ALLOWED = ['https://example.com'];
const io = new Server(httpServer, {
cors: { origin: ALLOWED },
allowRequest: (req, callback) => {
callback(null, ALLOWED.includes(req.headers.origin));
}
});
Originはブラウザ以外なら偽装できるため、これは他サイトのページからの接続を防ぐ層です。誰が接続してよいかは次の認証で判断します。
接続時の認証ミドルウェア
認証は接続確立時に走るio.use()のミドルウェアで行います。socket.handshake.authのトークンを検証し、失敗したらnext()にエラーを渡します。エラーに付けたdataはクライアントのconnect_errorまで届くので、ログイン切れと通信障害を分けて扱えます。
// サーバー
io.use(async (socket, next) => {
let user;
try {
user = await verifyToken(socket.handshake.auth.token);
} catch {
const err = new Error('認証に失敗しました');
err.data = { code: 'E_AUTH' };
return next(err);
}
if (!user) {
const err = new Error('認証に失敗しました');
err.data = { code: 'E_AUTH' };
return next(err);
}
socket.data.userId = user.id;
next();
});
// クライアント
const socket = io('https://example.com', {
auth: (cb) => cb({ token: localStorage.getItem('token') })
});
socket.on('connect_error', (err) => {
if (err.data?.code === 'E_AUTH') location.assign('/login');
});
authを関数にしておくと再接続のたびにトークンを読み直します。実測では、認証に失敗した接続はsocket.activeがfalseになり自動再接続されなかったため、トークンを取り直したらsocket.connect()を呼び直します。1メッセージの上限maxHttpBufferSizeは既定で1MBなので、大きなデータを送らない用途なら小さくします。
つまずいたときのデバッグ手順
接続できない、イベントが届かないときは次の順で切り分けます。
- ログを出す:Socket.IOの内部ログは既定で出ません。サーバーは
DEBUG=socket.io* node server.js、ブラウザは開発者ツールでlocalStorage.debug = 'socket.io-client:*'を実行して再読み込みします(Logging and debugging)。 connect_errorの中身を見る:xhr poll errorならCORSかURL・パスの誤り、websocket errorならプロキシのUpgrade設定、ミドルウェアのメッセージなら認証です。- 版を揃える:v2のクライアントは4.xのサーバーに既定では繋がりません。移行期間だけ
allowEIO3: trueで互換を有効にできます。 - 宛先を見直す:接続できているのに届かないなら、イベント名の綴り、
io.emitとsocket.broadcast.emitの取り違え、ルームへのjoin漏れを確認します。 - 台数を確認する:2台以上で一部にだけ届かない場合は、アダプターの設定と接続状態を確認します。ロングポーリングを使う場合は、スティッキーセッションも確認します。
JavaScript以外の言語から使う方法
公式ドキュメントには、Java・C++・Swift・Dart・Python・.NET・Rust・Kotlin・PHP・Goのクライアントと、Java・Python・Go・Rust・Denoのサーバー実装が並んでいます。どれもSocket.IOのプロトコルを話す専用ライブラリで、注意すべきはサーバーとの版の組み合わせです。
| 言語 | ライブラリ | 4.xサーバーと組む版 |
|---|---|---|
| Python | python-socketio | 5.x(最新5.17.0) |
| Java | socket.io-client-java | 2.x |
| .NET(C#) | SocketIOClient | v2・v3・v4サーバーに対応 |
| Java(サーバー) | netty-socketio(socketio4j) | 公式一覧で活発に開発中と表記 |
python-socketioの互換表では、JavaScript版3.x・4.xに対応するのは5.xです。JavaクライアントはREADMEで2.xがサーバー3.x・4.xに対応すると明記しています。DjangoならDjango Channelsという選択肢もあり、違いはDjango Channelsとは?ASGIとConsumer・Channel Layerの構成と採用判断で説明しています。
既存の業務システムにチャットや通知を組み込むなら、ルーム設計・認証・スケール構成を最初に決めておくと作り直しが減ります。設計から任せたい場合は業務用・Webアプリ開発でも相談を受けています。
よくある質問
socket.ioとsocket.io-clientの違いは何ですか?
socket.ioはNode.jsで動くサーバー用、socket.io-clientはブラウザやNode.jsから接続するクライアント用のパッケージです。どちらも最新は4.8.4で、サーバーとクライアントはメジャー版を揃えて使います。
socket.idは再接続しても同じですか?
通常の再接続では新しい値になります。connectionStateRecoveryを有効にして期限内に復元できた場合だけ、切断前と同じsocket.idが戻ります。利用者の識別には自前のユーザーIDを使ってください。
Next.jsでSocket.IOは使えますか?
使えます。ただしSocket.IOのサーバーは接続を保持し続ける常駐プロセスが前提なので、Next.jsのカスタムサーバーにSocket.IOを載せるか、Socket.IOだけ別のNode.jsプロセスとして立て、Next.jsのページからsocket.io-clientで接続する構成にします。
ReactでSocket.IOを使うときの注意点は?
接続はコンポーネントの外で1回だけ作り、useEffectで登録したリスナーはクリーンアップでsocket.off('イベント名', 関数)により外します。外し忘れると、Effectの再実行やコンポーネントの再マウント時にリスナーが重複し、同じイベントを何度も処理することがあります。関数を渡さずにoffすると他のコンポーネントのリスナーまで消える点も公式のReact連携ガイドが注意しています。
SockJSとSocket.IOの違いは?
どちらもWebSocketが使えない環境向けの代替通信を持つライブラリですが、プロトコルは別物で互いに接続できません。SockJSはWebSocketに近いAPIを提供する層で、ルームや確認応答は持ちません。npmのsockjs-clientは2022年5月の1.6.1が最新で、新規に選ぶならSocket.IOか素のWebSocketが現実的です。