That-Real-Time-Headache-Its-Not-The-WebSockets-Its-Your-Framework

Published: (December 28, 2025 at 09:57 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Introduction

I remember a few years ago I was leading a team to develop a real‑time stock‑ticker dashboard. 📈
Initially, everyone’s enthusiasm was incredibly high. We were all excited to build a live application with our own hands.

But soon we found ourselves stuck in the mud. The tech stack we chose performed reasonably well for ordinary REST APIs, but as soon as WebSockets came into the picture, everything became unrecognizable.

Our codebase split into two worlds:

  • Main application – handles HTTP requests.
  • Separate module – handles WebSocket connections.

Sharing state between these two worlds (e.g., a user’s login information) became a nightmare. We had to resort to clever (or rather, ugly) methods such as Redis or a message queue to synchronize data. 🐛 The code grew increasingly complex, and bugs multiplied. In the end we delivered the product, but the development process felt like a long, painful tooth extraction. 🦷

This experience taught me a profound lesson: for modern web applications that require real‑time interaction, how a framework handles WebSockets directly determines the development experience and the ultimate success or failure of the project. Many frameworks claim to “support” WebSockets, but most of them simply “weld” a WebSocket module onto the main framework. This “grafted” solution is often the root of all our headaches.

The Problems with “Grafted” Solutions

Java

// REST endpoint
@Path("/users")
public class UserResource {
    @GET
    public Response getUser(@Context SecurityContext sc) { … }
}

// WebSocket endpoint
@ServerEndpoint("/chat")
public class ChatEndpoint {
    @OnOpen
    public void onOpen(Session session) { … }
}
  • UserResource and ChatEndpoint live in two parallel universes.
  • Each has its own lifecycle, annotations, and injection mechanisms.
  • Getting the current user’s authentication information inside ChatEndpoint often requires digging into the underlying HTTP session—something the framework rarely lets you do easily. 😫

Node.js

// Express HTTP routes
const app = require('express')();
app.get('/profile', authMiddleware, (req, res) => { … });

// WebSocket server (ws)
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ noServer: true });

wss.on('connection', (ws, request) => {
  // No direct access to Express middleware here
});
  • app.get and wss.on('connection') are completely separate sets of logic.
  • Sharing middleware is cumbersome: you must manually invoke Express middleware during the WebSocket upgrade request.

State Sharing Becomes a Nightmare

The core of a real‑time application is state:

  • Which user corresponds to which WebSocket connection?
  • Which channels is the user subscribed to?

In a divided architecture:

  1. The REST API handles login and stores session data in HTTP‑session storage.
  2. The WebSocket module cannot directly access that session.

Result: you are forced to introduce external dependencies (e.g., Redis) as a “state intermediary.” This adds operational cost and new points of failure. 💔

A Natively Integrated WebSocket Framework: Hyperlane

Hyperlane treats a WebSocket handler exactly like any other HTTP route handler—a regular async function that receives a Context object. They are natural “siblings,” not distant relatives.

Consistent API

  • Middleware – Write it once, use it for HTTP and WebSocket routes.
  • Context – Holds request‑specific data (e.g., authenticated user).
  • Sending data – Use the same method for HTTP bodies, SSE events, and WebSocket messages:
ctx.send_body().await;

The framework abstracts away WebSocket protocol details (message framing, masking, etc.). You only need to care about the business payload (Vec).

Example: Authenticated WebSocket Route

// auth_middleware.rs
pub async fn auth_middleware(mut ctx: Context, next: Next) -> Result {
    let token = ctx.request.headers().get("Authorization");
    // …validate token…
    ctx.state.insert("user", user);
    next.run(ctx).await
}

// secure_websocket_route.rs
pub async fn secure_websocket_route(mut ctx: Context) -> Result {
    // The user was injected by `auth_middleware`
    let user = ctx.state.get::("user").unwrap();

    // Upgrade to WebSocket
    let mut ws = ctx.upgrade().await?;

    // Bind the connection to the user
    ws.on_message(move |msg| {
        // handle incoming messages, knowing `user`
    }).await;

    Ok(())
}
  • The HTTP upgrade request first passes through auth_middleware.
  • If the token is valid, the user information is stored in ctx.state.
  • Inside secure_websocket_route we retrieve the user without any glue code. 😎

Broadcasting to a Chat Room

Hyperlane’s documentation shows how to broadcast messages using a helper crate (e.g., hyperlane-broadcast). The pattern looks like this:

use hyperlane_broadcast::Broadcaster;

pub async fn chat_handler(mut ctx: Context) -> Result {
    let mut ws = ctx.upgrade().await?;
    let broadcaster = Broadcaster::new();

    ws.on_message(move |msg| {
        // Forward the message to all connected clients
        broadcaster.broadcast(msg);
    }).await;

    Ok(())
}

Technical tip: Register the broadcaster once at application start and share it via the Context to avoid creating multiple instances. This kind of “veteran” advice helps developers avoid common pitfalls. 👍

Conclusion

Real‑time functionality should no longer be a “special problem” in web development; it is a core component of modern applications. If your framework still forces you to handle WebSockets in a completely separate way, you’ll inevitably face the state‑sharing, middleware‑integration, and operational‑complexity headaches described above.

A framework that treats WebSockets as first‑class citizens—providing a unified API, shared middleware, and a single Context object—dramatically reduces boilerplate, eliminates external state intermediaries, and lets you focus on business logic instead of protocol gymnastics.

Hyperlane demonstrates how this can be done cleanly and efficiently. Give it a try, and you’ll see how much smoother real‑time development can be. 🚀

…a fragmented way, then it may no longer be suitable for this era.
A truly modern framework should seamlessly integrate real‑time communication into its core model. It should provide a consistent API, a shareable middleware ecosystem, and a unified state‑management mechanism. Hyperlane shows us this possibility.

So, the next time you have a headache developing real‑time features, please consider that the problem may not be with WebSockets themselves, but with the outdated framework you’ve chosen that still operates with a “grafted” mindset. It’s time for a change! 🚀

GitHub Home (link to the repository)

Back to Blog

Related posts

Read more »

Redis Pub/Sub vs Redis Streams

Redis provides multiple ways to handle messaging between services — with Pub/Sub and Streams being the two key built‑in options. Although they may look similar...