Skip to content
System Design

Why I Rewrote the Matching Layer with WebSockets

·8 min read

The initial version of ServiceBridge relied on HTTP short-polling for match notifications. Under load, this caused database connection pool exhaustion and 5+ second notification latency. Here is why I rewrote the matching layer with WebSockets.

The Problem: Stateless Polling at Scale

In a multi-sided marketplace, timing is everything. When a homeowner posts a job, the platform must notify nearby tradespeople immediately. The MVP used HTTP polling: clients pinged the server every 5 seconds asking, "Any new jobs?"

This worked fine at low scale. As concurrent active users grew, the architecture buckled. Every poll required an authentication check, a database query, and a JSON response.

  • Database Exhaustion: Thousands of queries per minute to return [] (no new matches).
  • Latency: A match could take up to 5 seconds to surface on the client, creating a race condition where users felt the app was unresponsive.
  • Wasted Bandwidth: High HTTP header overhead for empty payloads.

The Decision: Persistent Stateful Connections

I migrated from stateless HTTP polling to persistent stateful connections using WebSockets via Socket.io.

// WebSocket event architecture
export const setupMatchingGateway = (io: Server) => {
  io.on('connection', (socket) => {
    // Authenticate and join location-based room
    socket.join(`zone_${socket.user.serviceArea}`);

    socket.on('job_accepted', async (data) => {
      await handleJobAcceptance(socket.user.id, data.jobId);
    });
  });
}

Instead of clients asking the server if anything happened, the server pushes the event to the client the millisecond a match occurs. Result: sub-100ms dispatch notifications.

The Tradeoffs

The performance gains were immediate. But this came at the cost of infrastructure complexity: sticky sessions at the load balancer, mobile reconnection logic for users driving through dead zones, and a Redis Pub/Sub adapter so an event on Node A would reach a user connected to Node B.

Hindsight: I would have evaluated Server-Sent Events before committing to full bidirectional WebSockets. In the ServiceBridge architecture, client-to-server traffic was low relative to server-to-client push volume. SSE over HTTP/2 might have delivered the same latency benefits without the sticky-session complexity.