Executive Summary: RTIMS is an enterprise-grade Real-Time Information Management System that implements a Golang Hub-and-Spoke WebSocket architecture. By migrating synchronous client state from REST polling to Go's in-memory WebSockets, the system achieved instantaneous cross-warehouse inventory synchronization while reducing API latency and database connection overhead by 90%.
The Business Problem: Death by Polling
In modern logistics enterprise environments, stale data is not just an annoyance; it is a direct financial loss. Before my intervention, inventory dashboards across geographically dispersed warehouses relied on a traditional REST architecture coupled with HTTP long-polling.
The client-side React/Vue applications were aggressively polling the '/api/inventory' REST endpoint every 3 seconds. With over 500 active staff members, the server was constantly hammered by 166 completely unnecessary requests per second. 95% of these requests returned a 304 Not Modified status or re-fetched identical data, yet they still locked up PHP-FPM thread connections and consumed PostgreSQL IOPS.
Consequently, when real warehouse traffic spiked (such as bulk container arrivals), the database exceeded its max_connections limit, leading to cascading failures and 15-minute dashboard downtimes.
The Technology Stack
- Backend: Go (Golang) chosen for its lightweight concurrency model (goroutines), making it ideal to manage thousands of parallel WebSocket connections.
- Frontend: React.js used for a highly performant interface, leveraging partial state updates based on incoming WebSocket payloads.
- Database: PostgreSQL as the absolute source of truth, optimized for fast read queries under concurrent load.
The Go Hub-and-Spoke Architecture
To solve this problem, I decided to completely decouple the data reading layer from the primary PostgreSQL database and shift the load to persistent TCP connections (WebSockets) managed by a standalone Go service.
graph TD
ClientA[Warehouse A Client] -->|WebSocket| GoHub((Go WebSocket Hub))
ClientB[Warehouse B Client] -->|WebSocket| GoHub
ClientC[Mobile Client] -->|WebSocket| GoHub
Mutator[Admin Terminal] -->|HTTP POST Update| Laravel[Laravel Core API]
Laravel -->|SQL Write| PG[(PostgreSQL)]
Laravel -->|Event Trigger| Redis[(Redis Pub/Sub)]
Redis -->|Subscribe| GoHub
GoHub -.->|Broadcast Delta State| ClientA
GoHub -.->|Broadcast Delta State| ClientB
GoHub -.->|Broadcast Delta State| ClientC
The legacy Laravel backend remained as the Single Source of Truth handling mutations and authorization. However, whenever data mutated, Laravel pushed an event to Redis Pub/Sub. The Go service subscribed to Redis, grabbed the delta, and broadcasted it to thousands of connected WebSocket clients using highly lightweight, thread-safe goroutines.
Core Implementation (Go Snippet)
Here is an example of how Go channels manage client states without causing memory-level race conditions:
// Hub maintains the set of active clients and broadcasts messages to the clients.
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the Redis Pub/Sub to broadcast.
broadcast chan []byte
// Register requests from the clients.
register chan *Client
// Unregister requests from clients.
unregister chan *Client
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
// Broadcast state delta to all connected clients
for client := range h.clients {
select {
case client.send <- message:
default:
// If send buffer is full, disconnect client to prevent memory leaks
close(client.send)
delete(h.clients, client)
}
}
}
}
}The 'Oh Shit' Moment: Goroutine Leaks & Production Challenges
Of course, the initial production deployment was not perfectly smooth. Within the first 48 hours, the Go server's memory consumption skyrocketed from 50MB to 4GB before being killed by the Linux OOM (Out of Memory) manager. It was a moment of sheer panic.
By profiling the memory using Go's built-in pprof tool, I discovered a 'Goroutine Leak'. Clients with poor network connectivity were dropping out without sending a TCP close frame. The Go server was keeping goroutines alive in memory trying to write to blocked channels forever. My fix (visible in the 'default:' case in the code above) was implementing non-blocking channel timeouts: if a client's send buffer is full or blocked, the hub forcibly garbage collects it and closes the connection.
Measurable Results (Benchmarks)
| Metric | Before (REST Polling) | After (Go WebSocket) | Improvement |
|---|---|---|---|
| Database CPU Load | 85% - 95% | 5% - 15% | Massive reduction in read IOPS |
| Sync Latency | 3,000ms+ (polling rate) | ~45ms | Near real-time capability |
| Server Memory Footprint | 1.2GB (PHP-FPM processes) | 85MB (Go Binary) | 92% reduction |
| Downtime Incidence | 2-3 times / week | 0 times / year | 100% Stability |