Why we stopped running our own WebSocket servers
· Hibira
Self-run realtime servers jam when messages spike, and you chase patches for vulnerabilities like PHP2Shell yourself. What we ran into, what running your own needs, and what a hosted service takes on.

Before Hibira, we ran our own WebSocket servers on past projects, built in PHP and deployed as part of our monolith. Most of the time they were fine. When message volume spiked, they fell behind: the server was busy with the messages it already had, new requests queued up behind them, and latency kept climbing. By the time a message reached the user, it was already out of date. The price had moved, the score had changed, and the "typing…" indicator showed up after the other person had already sent their message.
This post is what we took away from that: why realtime servers jam at peak load, what you have to add if you run one yourself, and when handing it to a hosted service is worth it.
How the jam happens
What a realtime server does looks simple: take an event and send it to every subscribed connection. The trouble is that the work gets multiplied, and as soon as one part slows down, everything behind it waits.
Arriving faster than processed: the queue only grows
This is the basic rule of queues: as long as messages arrive faster than the server can process them, the queue keeps growing, and every new message waits for all the ones before it.
Put some numbers on it. Say the server can process 5,000 messages a second, and during a burst 6,000 a second arrive for 10 seconds. Those 10 seconds leave 10,000 messages unprocessed. Even if the burst ends and new messages stop entirely, the server needs another 2 seconds to clear them, so the last message in the queue goes out 2 seconds late. If normal traffic keeps arriving after the burst, it clears even more slowly.
On a web page, 2 seconds late is a worse experience. In a realtime feature, a price, position or game state from 2 seconds ago is often simply wrong.
One event, times every subscriber
Publishing once doesn't mean sending once. On a channel with 2,000 subscribers, every event you publish means 2,000 writes. The real workload is publishes per second × subscribers. One-to-many cases such as live chat rooms, match scores and operations dashboards are the ones most likely to tip over at the moment the audience grows.
One slow connection holds up everyone
A phone in an elevator or on the subway can slow to a crawl or stop receiving data for a while. If the server writes to connections one by one and waits for each write to finish before moving on, a single stuck connection delays every user after it. That was the "blocking" we ran into: the problem wasn't most users, it was a few slow connections dragging the whole broadcast down.
Waiting spreads
Once the server is busy, the HTTP requests that publish events slow down too. Your backend waits for them, so its own requests pile up. If timed-out requests are retried automatically, each retry adds more work to a server that is already overloaded. What started as a slow realtime server ends up as a slow system.
A monolith made it worse
Most of what we ran into came from deploying everything as one monolith: the WebSocket server ran and shipped together with the rest of the application. When realtime traffic surged, it used up the whole application's resources, so ordinary API requests queued too; and when another part of the app slowed down, realtime messages were held up with it. Scaling the realtime part meant adding machines for the entire application. And in a monolith, every deploy of the app usually drops and reconnects every WebSocket connection as well.
The other reason: security updates for the runtime
Besides performance, the other reason we stopped running our own was security. We also ran into PHP security vulnerabilities back then and had to chase the patches ourselves. When you run your own servers, every vulnerability in the operating system, the language runtime and the libraries is yours to track, and none can slip.
The latest example is the pair of critical PHP vulnerabilities, known as "PHP2Shell", that the PHP project fixed on October 21, 2026:
- CVE-2026-109618: memory corruption while handling out-of-memory (OOM) conditions.
- CVE-2026-109619: a use-after-free caused by state left over between requests.
Both score 9.2 on CVSS v4.0 and can let an unauthenticated attacker execute code remotely. They affect PHP serving web requests, such as PHP-FPM, Apache mod_php and php-cgi. The fixed versions are 8.2.35, 8.3.36, 8.4.27 and 8.5.12; 8.1 and older no longer get routine official updates.
These two are in PHP serving web requests, so they don't necessarily hit a long-running WebSocket process directly. The lesson is the same, though: the moment a vulnerability is public, you have to work out which servers are affected, schedule the upgrade and redeploy, as fast as you can.
If you run it yourself, add these
Each of these problems has a fix, but every fix is ongoing work, not a setting you change once:
- Deploy it apart from your application. Take the realtime server out of the monolith so it scales and deploys on its own, and a peak can't take the rest of your app down with it.
- A bounded queue per connection. Writes must not wait on each other. Each connection gets its own queue; when it fills up, that client can't keep up and should be disconnected rather than allowed to slow everyone else.
- Separate receiving from sending. The publish API shouldn't wait until every subscriber has the message before it answers, or a slow broadcast slows your backend too.
- Horizontal scaling. A single server always has a ceiling. Adding machines means every machine has to share channels and presence, with a load balancer in front.
- Test capacity against peaks. A load test at average traffic tells you everything is fine. Test several times your peak, and many clients reconnecting at once.
- Monitor queues and latency, not just CPU. The queue can be growing while CPU still looks fine. You need to see how long messages wait and how many connections are dropped for being too slow, and get alerted when it gets worse.
- Someone on call. The peak is usually your most important moment: a ticket sale opening, the last minute of a match, a promotion going live. Someone has to be there when the server has trouble.
- Keep up with security updates. When a vulnerability in the operating system, language runtime or libraries goes public, assess it, upgrade and redeploy.
Writing one by one
A queue per connection
What a hosted service takes on for you
With a hosted realtime service, the provider does the work above. Taking Hibira as the example:
- Slow clients don't slow anyone else. Each connection has its own bounded queue, and writes never wait on each other. A client whose queue fills up or whose writes time out is closed with the Pusher protocol's 4100 error code, and pusher-js reconnects on its own when it gets it.
- You can see what happened. These disconnects show up on the app's errors card as "Slow client disconnected", so you don't have to dig through server logs.
- We plan the capacity. The servers run as a cluster that shares channels and presence and scales horizontally. Plan limits are clear hard limits, so there are no surprises on the bill.
- It speaks the Pusher protocol. Your existing pusher-js, Laravel Echo and server libraries work as they are; switching services means changing the host and keys.
- We do the patching. Security updates for the WebSocket servers and the runtime under them are ours to handle. You still update your own app, but there is one less thing to watch.
The costs should be said plainly too: past the free tier you pay monthly, your events pass through a third party, and you live with the plan's limits.
When running your own still makes sense
A hosted service isn't the only answer. Running your own may suit you better when:
- Your traffic is small and steady, without sharp peaks.
- Event contents can't leave your infrastructure.
- You already have an operations team, monitoring and an on-call rotation, so one more long-running service is no burden.
If you use Laravel, run Reverb yourself or use a hosted service compares the two in more detail. Both speak the Pusher protocol, so changing your mind later is only a configuration change.
Our takeaway
So we decided to rebuild: we took realtime messaging out of the monolith and made it a separate service. A load balancer sits in front, and the service scales out automatically with load, so a sudden surge is spread across more servers instead of one server straining to keep up. We used it internally first, then opened it up to everyone else. That is Hibira.
The hard part of running your own WebSocket server isn't getting it to run. It's keeping it on time during the busiest few minutes, which are exactly when users care most and when things are most likely to break. If your team would rather spend its time on the product than on queues and on-call, a hosted service is the easier choice.
Hibira's free plan needs no credit card and is enough for development and small apps. If you already use Pusher, follow Migrating from Pusher Channels to switch over without changing your code.