WordPressでチャットアプリを作ろうとしたとき、私は最初から「リアルタイム=WebSocket」だと思い込んでいました。Slackみたいに、メッセージが流れて、未読が付いて、ファイルも飛んでくる。あの体験を作るなら、双方向の常時接続でしょ、と。
ところが本番環境はXserver(共用レンタル)。ここで私は、通信方式の“理想”と、インフラの“現実”の差にぶつかります。この記事は、その試行錯誤と挫折、そしてLong Pollingに落ち着くまでの体験談です。
第1章:WebSocketで行けると思った(そして最初の挫折)
実装の序盤は順調でした。ブラウザ側でWebSocketを開き、サーバ側はイベントを受けたら全員にブロードキャスト。ローカル環境だと「うわ、チャットっぽい!」がすぐ出ます。
WebSocket(ブラウザ側)最小サンプル
// WebSocket(ブラウザ側)最小サンプル
const ws = new WebSocket("wss://example.com/ws");
ws.addEventListener("open", () => ws.send("hello"));
ws.addEventListener("message", (ev) => console.log("recv:", ev.data));
ws.addEventListener("close", () => console.log("closed"));
ws.addEventListener("error", (e) => console.log("error", e));
(参考)WebSocketサーバ最小(Node.js)
※WebSocketは「別プロセス(常駐)」で待ち受ける形になりやすいです。共用レンタルではこの“運用前提”が壁になりがちです。
// WebSocket(サーバ側)最小サンプル(Node.js + ws)
// npm i ws
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket) => {
socket.on("message", (msg) => {
for (const client of wss.clients) {
if (client.readyState === 1) client.send(msg.toString());
}
});
});
console.log("ws server on :8080");
で、私は「よし本番へ」とXserverに上げて、ブラウザで開きました。
……繋がらない。
コンソールには、WebSocketハンドシェイクが通らないようなエラー。HTTP的には400系の匂い。私はここで初めて「WebSocketって、アプリだけの問題じゃないんだ」と思い知らされます。
なぜ動かない? 私がやった切り分け
- wssか?(HTTPSサイト上でws://に繋いでいないか)
- 同一ドメインか?(CORS/終端/証明書のねじれ)
- どこで落ちてる?(ブラウザ→中間装置→サーバ、どの層か)
このとき私の中で決定打になったのが、「XserverではWebSocketが400になり、pollingなら動く」という実例を見つけたことでした。WebSocketはHTTPからUpgradeして別プロトコルへ切り替える性質があり、このUpgradeが中間装置(プロキシ/WAF等)で止まると成立しません。
つまり私の挫折は「コードが悪い」というより、共用レンタルの構造上、自分が触れない層で詰まっている可能性が高かった。
ここで私はWebSocketを諦めました。正確には、「この環境でWebSocket前提の設計にしない」方針へ転換しました。
第2章:じゃあSSEなら…と思った(そして2回目の挫折)
WebSocketがダメなら、片方向でもいい。チャットの“受信”さえリアルタイムにできれば、送信はHTTP POSTでも成立する。
そう考えて、私はSSE(Server-Sent Events)に希望をかけました。SSEはHTTPのまま接続して、サーバ→ブラウザへイベントを流し続けられます。Upgradeしないなら、通る余地がある。そう思ったんです。
SSE(PHP:サーバ側)最小サンプル
<?php
// SSE(PHP)最小サンプル
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");
header("Connection: keep-alive");
// 可能ならバッファを切る(効かない環境もあります)
@ini_set("output_buffering", "off");
@ini_set("zlib.output_compression", 0);
while (ob_get_level() > 0) { @ob_end_flush(); }
@ob_implicit_flush(true);
$i = 0;
while (true) {
$i++;
echo "event: msg\n";
echo "data: " . json_encode(["n" => $i, "ts" => time()]) . "\n\n";
flush();
usleep(1000 * 1000); // 1秒
}
SSE(ブラウザ側)最小サンプル
// SSE(ブラウザ側)最小サンプル
const es = new EventSource("/sse.php");
es.addEventListener("msg", (ev) => console.log("msg:", ev.data));
es.addEventListener("error", (e) => console.log("sse error", e));
ローカルでは、これも気持ちよく動きます。ログが毎秒増える。イベントが流れる。「これだ」と思いました。
しかし、Xserverに上げた途端に雰囲気が変わります。
- 最初は繋がるけど、途中で止まる
- 送っているはずのイベントが、リアルタイムに届かない(溜まって後から来る)
私はまた「なぜだろう?」に戻りました。
原因の当たり:タイムアウトとバッファリング
SSEは“レスポンスを閉じない”通信です。つまり長時間接続。ここで問題になるのが、中間装置のタイムアウトとバッファリングです。
たとえばNginxのリバースプロキシ配下では、SSEがバッファリングされて「即時に届かない」現象が起こり得ます。対策としてヘッダでバッファリングを無効化する(例:X-Accel-Buffering: no)などが紹介されていますが、共用レンタルでは“その層の設定”が自分の手から外れていることが多い。
私はここで理解しました。SSEは「仕組みとしてはHTTP」でも、実運用ではプロキシやミドルウェアの設定とセットになりがちだ、と。
WebSocketと同じです。やりたいことは正しい。でも、安定化のスイッチが自分の手の届かないところにある。
この時点で、私はまた方針を変えます。
第3章:しかたない、Long Pollingにする(そしてようやく“運用”になった)
WebSocketもSSEも「長時間接続」を前提にしています。共用レンタルの現実では、長時間接続は不確定要素が増える。
ならば、私は普通のHTTPで完結する方式に寄せるべきだ。
そこで最後に選んだのがLong Polling(ロングポーリング)です。名前は強そうですが、やっていることは地味です。
- 「新着ある?」とHTTPで問い合わせる
- 新着がなければ最大15〜20秒くらい待つ
- 返ったらすぐ次の問い合わせを投げる
これが、Xserverではすんなり通りました。なぜなら、Long PollingはUpgradeしないし、永遠に開きっぱなしにしない。見た目が“普通のHTTP”なので、共用レンタルの得意分野に収まるからです。
Long Poll(PHP:サーバ側)最小サンプル
<?php
// Long Polling(PHP)最小サンプル:最大18秒待ってJSONを返す
header("Content-Type: application/json; charset=utf-8");
header("Cache-Control: no-store");
$since = isset($_GET["since"]) ? (int)$_GET["since"] : 0;
$deadline = microtime(true) + 18.0;
function get_new_messages($since) {
// DB検索に置き換える(例:id > $since)
if (time() % 5 === 0) {
$id = $since + 1;
return [["id" => $id, "text" => "new message " . $id, "ts" => time()]];
}
return [];
}
$messages = [];
$last_id = $since;
while (microtime(true) < $deadline) {
$messages = get_new_messages($since);
if (!empty($messages)) { $last_id = end($messages)["id"]; break; }
usleep(250000); // 250ms
}
echo json_encode(["messages" => $messages, "last_id" => $last_id], JSON_UNESCAPED_UNICODE);
Long Poll(ブラウザ側)最小サンプル
// Long Polling(ブラウザ側)最小サンプル(連打防止&バックオフ入り)
let lastId = 0;
let backoff = 300;
async function poll() {
while (true) {
if (document.hidden) { await sleep(1500); continue; }
try {
const res = await fetch(`/poll.php?since=${lastId}`, { cache: "no-store" });
const data = await res.json();
if (Array.isArray(data.messages) && data.messages.length) {
for (const m of data.messages) console.log("recv:", m);
lastId = data.last_id;
} else {
await sleep(60); // 空は少し休む(連打防止)
}
backoff = 300;
} catch (e) {
console.log("poll error", e);
await sleep(backoff);
backoff = Math.min(8000, Math.floor(backoff * 1.6));
}
}
}
function sleep(ms){ return new Promise(r => setTimeout(r, ms)); }
poll();
私がLong Pollingで意識したこと(運用の現実)
- 待ち時間は短め(15〜20秒)…長くしすぎると逆に不安定になりやすい
- 空レスポンス連打を防ぐ…少しsleepを入れて負荷を抑える
- タブ非表示は間引く…見てない間に回し続けない
- エラー時はバックオフ…障害時に自分で炎上を広げない
正直、WebSocketの“ぬるっとしたリアルタイム感”には勝てません。でも、私が欲しかったのは「理想のリアルタイム」より「止まらないチャット」でした。
エピローグ:技術の勝ち負けじゃなく、環境との相性だった
- WebSocket:最高に気持ちいい。でもUpgradeと常時接続の前提が重い
- SSE:HTTPのままでも、結局は長時間接続でタイムアウト/バッファリングの壁が出る
- Long Polling:地味。でもHTTPの範囲に収まり、共用レンタルで成立しやすい
もし「Xserver+WordPressでチャット」をやるなら、私はまずLong Pollingで“運用できる完成”を作り、必要になったタイミングでVPS/専用へ移行してWebSocket/SSEを再検討する、という順番をおすすめします。


コメント