※本記事には広告(楽天アフィリエイト)リンクが含まれる場合があります。

チャット・在庫通知・株価表示・タスクボードのライブ更新。2026年のWebアプリで「リアルタイム」は当たり前の要件になりました。しかし「サーバーからデータを取得する」HTTPリクエストの知識だけでは、サーバー側からクライアントへ自発的にデータを送る仕組みを実装できません。

リアルタイム通信の主役は、WebSocket(双方向・常時接続)とSSE(Server-Sent Events)(サーバー→クライアント一方通行・HTTPのまま)の2つです。どちらもブラウザ標準でサポートされており、個人開発の規模なら無料枠の範囲で十分に本格的な機能を作れます。加えて2026年は、Cloudflare Durable Objectsのようなエッジでリアルタイム状態を管理する仕組みが身近になり、サーバーレス構成でもチャットが作れる時代になりました。

本記事では、ポーリング・SSE・WebSocketの違いから始めて、素のWebSocket APIでのチャット実装、Socket.IOによるルーム管理・再接続、SSEの自動再接続とイベントID、Cloudflare Durable Objectsでのエッジ実装、そして本番運用で必ず押さえたいコネクション管理・認証・スケーリングまで、個人開発者が「今すぐ使える」コード付きで完全解説します。

広告・楽天アフィリエイト

ノートパソコン特集

開発用ノートPCをスペック・口コミで比較。

楽天ポイント還元

ノートPCを楽天で見る →

1. リアルタイム通信の3つの選択肢:ポーリング・SSE・WebSocket

まず「リアルタイムに見せる」ための技術を整理します。どれも「サーバーに新しいデータがあるか確認する」手段ですが、誰が・いつ・どちら向きに通信するかが決定的に違います。

項目 ポーリング(短間隔) SSE(Server-Sent Events) WebSocket
方向 クライアント→サーバー サーバー→クライアント(一方通行) 双方向
プロトコル HTTP(GET) HTTP(通常のGET + text/event-stream) ws:// / wss://(HTTP Upgrade)
常時接続 ✗(毎回切断) ◯(自動再接続あり) ◯(コネクション維持)
クライアント実装 fetch / setInterval EventSource(ブラウザ標準) WebSocket API(ブラウザ標準)
逆方向(クライアント→サーバー) そのまま送れる ✗(別途HTTPで送る) 同じコネクションで送れる
遅延 間隔次第(数秒〜) ほぼ即時(サーバー→) ほぼ即時(双方向)
相性の良い用途 更新頻度が低い・負荷を気にしない時 通知・フィード・株価・進捗表示 チャット・ゲーム・共同編集

2026年の実務の目安は以下の通りです。

  • サーバーから通知を一方的に流したいだけ(新着記事・在庫復活・ジョブ完了)→ SSE。実装が最も簡単で、CDNやプロキシとの相性も良い
  • 双方向のやり取りが必要(チャット・フォームのライブ同期・対戦ゲーム)→ WebSocket
  • 頻度が低い・リアルタイム性が要らない(5分に1回の更新など)→ 普通にポーリングか、下記のWebhookで済ませる

「どちらでも良さそう」なケースでは、まずSSEで始めて、双方向が必要になってからWebSocketに移行するのが個人開発では現実的です。以下、それぞれを実装コード付きで見ていきます。

2. 素のWebSocket APIでチャットを作る

まずは余計なライブラリなしで、ブラウザ標準のWebSocketと、Node.jsのwsパッケージだけでシンプルなチャットを実装します。ここで接続・送受信・切断の基本を理解すると、Socket.IOなどを使うときも土台が分かった状態で進められます。

2.1 サーバー側(Node.js + ws)

// server.mjs
import { WebSocketServer } from 'ws'

const wss = new WebSocketServer({ port: 8080 })
const clients = new Set()

wss.on('connection', (ws) => {
  clients.add(ws)
  console.log('接続:', ws._socket.remoteAddress)

  // クライアントからのメッセージを受信
  ws.on('message', (raw) => {
    let msg
    try {
      msg = JSON.parse(raw.toString())
    } catch {
      return // JSONでなければ無視
    }
    // 全クライアントへブロードキャスト
    const out = JSON.stringify({ user: msg.user, text: msg.text, at: Date.now() })
    for (const c of clients) {
      if (c.readyState === c.OPEN) c.send(out)
    }
  })

  ws.on('close', () => {
    clients.delete(ws)
    console.log('切断')
  })

  ws.on('error', (err) => console.error(err))
})

console.log('ws://localhost:8080 で待機中')

ポイントはclientsというSetで接続中のソケットを管理し、メッセージを受けたら全員にsendしている点です。これが「ブロードキャスト」の基本形です。後述のSocket.IOではこの辺りがもっと便利になります。

2.2 クライアント側(ブラウザ標準API)

// client.js — 素のWebSocket API
const ws = new WebSocket('wss://example.com/ws')

ws.onopen = () => {
  console.log('接続完了')
  sendMessage('こんにちは')
}

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  appendMessage(msg.user, msg.text)
}

ws.onclose = (event) => {
  // 1000 = 正常クローズ。それ以外なら再接続を検討する
  console.log('切断:', event.code, event.reason)
}

ws.onerror = (err) => console.error('WebSocketエラー:', err)

function sendMessage(text) {
  if (ws.readyState !== WebSocket.OPEN) return
  ws.send(JSON.stringify({ user: 'yamada', text }))
}

ここで注意したいのはreadyStateです。WebSocket.OPEN(1)以外の状態でsendすると例外になります。また、oncloseevent.codeで切断理由が分かるので、「ネットワーク断なら自動再接続」のような処理を入れる際の判断材料になります。

2.3 再接続とハートビート(ping/pong)

素のWebSocketを本番で使うなら、自動再接続ハートビートはほぼ必須です。モバイル回線の切り替わりやプロキシのタイムアウトで、コネクションは普通に切れます。

// 指数バックオフ付き自動再接続 + ハートビート
let ws
let retry = 0

function connect() {
  ws = new WebSocket('wss://example.com/ws')

  ws.onopen = () => {
    retry = 0
    console.log('接続完了')
    // 30秒ごとにpingを送り、応答がなければ切断扱いにする
    heartbeatTimer = setInterval(() => {
      if (ws.readyState === WebSocket.OPEN) ws.send('ping')
    }, 30_000)
  }

  ws.onmessage = (e) => {
    if (e.data === 'pong') return // ハートビート応答は無視
    handleMessage(e.data)
  }

  ws.onclose = () => {
    clearInterval(heartbeatTimer)
    const delay = Math.min(1000 * 2 ** retry, 30_000) // 1s→2s→4s...最大30s
    retry++
    setTimeout(connect, delay)
  }
}
connect()

使ってみた感想: 素のWebSocketは「ライブラリの依存を増やさない」という点では潔いのですが、再接続・リトライ・ルーム・認証・切断検知を全部自分で実装することになり、チャット1つでもコードが膨らみます。小規模な通知や実験用途ならこれで十分ですが、ちゃんとしたチャットやコラボ機能を作るなら次のSocket.IOが断然楽です。

広告・楽天アフィリエイト

PCモニター

デュアルディスプレイや高解像度モデルをチェック。

楽天ポイント還元

モニターを楽天で見る →

3. Socket.IOでルーム・認証・再接続を一気に解決する

Socket.IOはWebSocketの上に「ルーム」「自動再接続」「イベントベースの通信」「フォールバック(HTTPロングポーリング)」を載せた実用ライブラリです。2026年もリアルタイムWebの定番で、個人開発のチャット・通知・コラボ機能で最も導入実績があります。

3.1 サーバー側(ルーム付きチャット)

// server.mjs — Socket.IO
import { Server } from 'socket.io'
import { createServer } from 'node:http'

const httpServer = createServer()
const io = new Server(httpServer, {
  cors: { origin: process.env.CLIENT_ORIGIN || 'http://localhost:5173' }
})

io.use((socket, next) => {
  // ミドルウェアで認証(ここでは簡易版。本番はJWT検証)
  const token = socket.handshake.auth?.token
  if (!token) return next(new Error('unauthorized'))
  socket.data.user = verifyToken(token) // ユーザー情報を socket.data に保持
  next()
})

io.on('connection', (socket) => {
  const user = socket.data.user
  console.log(`${user.name} が接続`)

  // 「room:join」イベントで任意のルームに入る
  socket.on('room:join', (roomId) => {
    socket.join(roomId)
    socket.to(roomId).emit('system', { text: `${user.name} が参加しました` })
  })

  socket.on('chat:message', ({ roomId, text }) => {
    // 同じルームのメンバーだけに配信
    io.to(roomId).emit('chat:message', {
      user: user.name,
      text,
      at: Date.now()
    })
  })

  socket.on('disconnect', (reason) => {
    console.log(`${user.name} 切断:`, reason)
  })
})

httpServer.listen(3000)

Socket.IOの真骨頂はio.to(roomId).emit(...)ルーム配信です。「特定のチャットルーム」「特定のユーザー宛て(io.to(socketId))」だけに送る処理が、数行で書けます。

3.2 クライアント側

// client.js — Socket.IOクライアント
import { io } from 'socket.io-client'

const socket = io('https://example.com', {
  auth: { token: localStorage.getItem('token') },
  transports: ['websocket', 'polling'], // フォールバック順
  reconnection: true,
  reconnectionAttempts: Infinity,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 10000
})

socket.on('connect', () => {
  console.log('接続ID:', socket.id)
  socket.emit('room:join', 'room-123')
})

socket.on('chat:message', (msg) => appendMessage(msg))
socket.on('system', (msg) => appendSystem(msg))
socket.on('connect_error', (err) => console.error('接続エラー:', err.message))

function sendMessage(text) {
  socket.emit('chat:message', { roomId: 'room-123', text })
}

使ってみた感想: 素のWebSocketで書いていた「再接続」「ハートビート」「エラー時のリトライ」がSocket.IOでは設定項目になるだけで済みます。connect_errorで認証エラーを拾えるのも便利です。なお、認証トークンはauthオプションで渡し、サーバー側ミドルウェアで検証するのが2026年の標準パターンです。

4. SSE(Server-Sent Events)で通知・フィードを実装する

一方通行でいいなら、SSEの方がWebSocketよりずっと簡単です。ブラウザのEventSource自動再接続を標準装備しており、サーバー側も普通のHTTPエンドポイントで実装できます。

4.1 サーバー側(Node.jsの素のHTTPで)

// sse-server.mjs
import { createServer } from 'node:http'

const subscribers = new Set()

createServer((req, res) => {
  if (req.url === '/events') {
    // SSEのレスポンスヘッダー
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive'
    })
    res.write('retry: 3000\n\n') // 再接続間隔(3秒)
    subscribers.add(res)
    req.on('close', () => subscribers.delete(res))
    return
  }

  if (req.url === '/notify') {
    // 外部から通知を流す例(POST想定)
    const data = JSON.stringify({ type: 'notice', text: '新しい記事が出ました' })
    const payload = `id: ${Date.now()}\nevent: notice\ndata: ${data}\n\n`
    for (const res of subscribers) res.write(payload)
    res.writeHead(200)
    res.end('ok')
    return
  }

  res.writeHead(404)
  res.end()
}).listen(3000)

SSEのレスポンス形式はdata:(データ本体)、event:(イベント名)、id:(イベントID)、retry:(再接続間隔)を\n\nで区切るだけという、きわめてシンプルなテキスト形式です。id:を送っておくと、再接続時にLast-Event-IDヘッダーでクライアントが「どこまで受信したか」をサーバーに伝えられます。

4.2 クライアント側(EventSource)

// client.js — EventSource(自動再接続つき)
const es = new EventSource('/events')

// 名前なしのデータ
es.onmessage = (event) => {
  console.log('受信:', event.data)
}

// 名前付きイベント(server側で event: notice と送った場合)
es.addEventListener('notice', (event) => {
  const data = JSON.parse(event.data)
  showNotification(data.text)
})

// 接続状態の変化(自動再接続はブラウザが処理してくれる)
es.onerror = () => {
  console.log('接続が切れました。自動的に再接続されます')
}

使ってみた感想: チャットの「未読通知バッジ」「タスクの完了通知」「ジョブの進捗バー」など、サーバー→クライアント一方通行の用途ならSSEが最適です。WebSocketと違って「クライアントから送る」手段がないため、投稿操作は通常のfetchで行い、その結果をSSEで周囲に流す、という構成になります。

5. Cloudflare Durable Objectsでエッジにチャットを作る

2026年の個人開発で特に熱いのが、Cloudflare Workers + Durable Objectsによるエッジリアルタイム実装です。従来「WebSocketを張り続けるサーバー」が必要だったチャットを、サーバーレスで実現できます。Cloudflare WorkersでマイクロSaaSを作ると併せて読むと理解が深まります。

5.1 Durable ObjectでWebSocket接続を集約する

// chat.js — Cloudflare Workers + Durable Objects
export class ChatRoom {
  constructor(state, env) {
    this.state = state
    this.sessions = new Map() // id -> WebSocket
  }

  async fetch(request) {
    const url = new URL(request.url)

    // WebSocket接続(チャットルームごとに1つのDOが担当)
    if (url.pathname === '/ws' && request.headers.get('Upgrade') === 'websocket') {
      const [client, server] = Object.values(new WebSocketPair())
      server.accept()
      const id = crypto.randomUUID()
      this.sessions.set(id, server)

      server.addEventListener('message', (event) => {
        // 接続中の全クライアントにブロードキャスト
        for (const s of this.sessions.values()) {
          s.send(event.data)
        }
      })

      server.addEventListener('close', () => {
        this.sessions.delete(id)
      })

      return new Response(null, { status: 101, webSocket: client })
    }

    return new Response('Not Found', { status: 404 })
  }
}

// Worker本体: /api/room/:id/ws をDOにルーティング
export default {
  async fetch(request, env) {
    const url = new URL(request.url)
    const roomId = url.pathname.split('/')[2] || 'default'
    const id = env.CHAT_ROOM.idFromName(roomId)
    const stub = env.CHAT_ROOM.get(id)
    return stub.fetch(request)
  }
}

Durable Objectsの利点は、同じルームにいるユーザーの接続が1つのDOに集約される点です。DOはエッジ(東京リージョンなど)で状態を持ち続けるため、「チャット履歴をメモリに持つ」「接続数を数える」といったことがサーバーレスで可能になります。無料枠でも小規模チャットは十分動きます(有料プランでもDOの課金は接続時間ベースで安価です)。

履歴の永続化にはCloudflare D1R2Upstash Redisを組み合わせるのが定番構成です。「DOが最新の接続状態を、DBが履歴を」という役割分担が分かりやすくておすすめです。

6. 本番運用のための実践テクニック

ここからは、どの技術で作る場合でも必要になる「本番で壊れない」ためのポイントをまとめます。

6.1 コネクション管理と上限

  • 接続数を把握する — サーバー側で接続数をメトリクスに出し、上限(例: 1プロセスあたり数万)を超えたら新規接続を拒否する。エラーモニタリングはSentryなどのツールで可視化する
  • ハートビートで死に接続を掃除する — 素のWebSocketならping/pong、Socket.IOならデフォルトのpingInterval/pingTimeoutに任せる
  • アイドル接続のタイムアウト — 長時間データが流れない接続は、プロキシ(nginx, Cloudflare)に切られることがある。定期的にダミーデータかpingを流す

6.2 認証と認可

WebSocketもSSEも、接続時にトークンを検証するのが基本です。クエリ文字列にトークンを入れるとログに残るため、Socket.IOのようにauthで渡すか、Cookie(SameSite属性つき)で渡すのが安全です。ルームごとの参加権限(このルームに入れるのは誰か)は、join処理のたびにサーバー側で検証しましょう。認証まわりは認証サービス比較Passkeysガイドも参考になります。

6.3 スケーリング:複数サーバーになったら

Socket.IOを複数インスタンスで動かす場合、io.to(room).emitのルーム情報をRedis(アダプタ)で共有する必要があります。Upstash Redisならサーバーレスで@socket.io/redis-adapterを接続できます。逆に「最初からスケールを考えたくない」なら、Cloudflare Durable Objectsのように接続を1箇所に集約する設計を選ぶ手もあります。

// 複数インスタンス対応: Socket.IO + Redisアダプタ
import { Server } from 'socket.io'
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient } from 'redis'

const pubClient = createClient({ url: process.env.REDIS_URL })
const subClient = pubClient.duplicate()
await Promise.all([pubClient.connect(), subClient.connect()])

const io = new Server(httpServer, { adapter: createAdapter(pubClient, subClient) })

6.4 レート制限とメッセージサイズ

リアルタイムAPIはスパム攻撃の標的になりやすいため、送信レート制限(例: 1秒間に5メッセージまで)と最大メッセージサイズ(例: 8KB)を必ず設けましょう。WebSocketのwsならmaxPayloadオプション、Socket.IOならmaxHttpBufferSizeで制限できます。API全体のレート制限の設計はAPI設計ベストプラクティスで詳しく解説しています。

7. まとめ:リアルタイム機能は「向き」で選ぶ

最後に、リアルタイム通信の選び方を整理します。

  1. 一方通行の通知・フィードなら SSE — 自動再接続つき・HTTPのまま・実装最小。まずここから
  2. 双方向チャット・共同作業なら WebSocket — 素のAPIで基本を学び、本格化するならSocket.IO(ルーム・認証・再接続が楽)
  3. サーバーレスで作りたいなら Cloudflare Durable Objects — エッジで接続を集約。履歴はD1/R2/Upstashと組み合わせる
  4. 本番で必ずやること — ハートビート・自動再接続・トークン検証・レート制限・接続数の監視
  5. 非同期処理と組み合わせる — 「ジョブ完了を通知する」ならバックグラウンドジョブとSSEの組み合わせが鉄板。サーバー間のイベント通知はWebhookも選択肢

リアルタイム機能は最初は「難しい技術」に見えますが、SSEなら30分、WebSocket + Socket.IOでも半日あれば動くものが作れます。まずは「通知をSSEで流す」→「チャットをSocket.IOで作る」→「必要ならDurable Objectsへ移行」の順で進めるのが、個人開発者が失敗しない近道です。

関連記事として、Reactデータフェッチ完全ガイド(リアルタイムデータのキャッシュ設計)、Supabaseガイド(リアルタイム機能付きBaaS)、Webhook実装ガイドもあわせてどうぞ。快適なリアルタイム開発を始めましょう。