When building instant messaging (IM) systems, the biggest headache for developers is often not how to send messages, but how to manage connections. Im...
Guide to pitch-avoidance: How to gracefully handle "multi-terminal login" and connection competition in the IM system?
Published: 2026-06-20 (a month ago)
GO

When building Instant Messaging (IM) systems, what causes developers the most headache is often not how to send messages, but how to manage connections.

Imagine a scenario: user Xiao Ming is chatting on their phone, and suddenly opens their computer to log in. At this moment, should the system "kick" the mobile client offline? Or should it allow both clients to be online simultaneously? If the user's network fluctuates, generating two reconnection requests in a short period of time, how does the server ensure that it won't face the awkward situation where "the old connection is not disconnected, and the new connection cannot be established"?

Today, we will talk about how to elegantly resolve multi-device connection competition using Go, through Axiom's engineering practices.


1. Modeling: From "One-to-One" to "One-to-Many"

Most beginners simply design connection management as a map[UserID]Connection. However, this is not viable in modern internet scenarios.

[!IMPORTANT]
Deep Dive: A real multi-device system needs to support a dual index of "UserID + DeviceID".

In our project, the connection manager (Manager) adopts a nested map structure:

go Copy
type Manager struct {
    // UserID -> DeviceID -> Client
    Clients map[uint]map[string]*Client
    Lock    sync.RWMutex
}

This structure allows the same user to be online on multiple devices such as Web, iOS, and Android at the same time, laying the physical foundation for multi-device message synchronization.


2. Core Tactics: "Preempt" First, "Clean Up" Later

When a new WebSocket connection request comes in, the most elegant way to handle it is not to wait with a giant lock, but to use optimistic preemption.

2.1 Preemptive Registration

In the main loop of the Manager, our logic for handling new connections (Register) is as follows:

  1. Save new connection: Directly store the new Client instance in the map, overwriting the old record for the same device.
  2. Asynchronously close old connection: If it is found that the device already had an active connection previously, instead of immediately blocking to wait, we get a reference to the old connection.
  3. Trigger self-cleanup: Call Close() on the old connection.

2.2 Why is this more elegant?

In Go, closing a network connection triggers its corresponding read Goroutine to exit, thereby automatically triggering the unregistration (Unregister) process. This "leveraging external forces" design avoids writing complex cleanup code inside the registration logic, keeping the main loop lightweight.


3. Devil in the Details: Preventing "Collateral Damage"

In high-concurrency scenarios, a bizarre situation may arise: connection A is disconnecting, while connection B is connecting at almost the same instant. If the logic is handled improperly, the offline logic of connection A might delete the newly connected connection B.

[!WARNING]
Deep Trap Warning: In the Unregister logic, an object pointer comparison must be performed.

go Copy
case client := <-m.Unregister:
    m.Lock.Lock()
    if devices, ok := m.Clients[client.UserID]; ok {
        // Key: only perform deletion if the connection object in current memory is the exact same one to be unregistered
        if current, exists := devices[client.DeviceID]; exists && current == client {
            delete(devices, client.DeviceID)
        }
    }
    m.Lock.Unlock()

This "object-level" check ensures that even if millisecond-level race conditions occur, the system can accurately identify who should leave and who should stay, avoiding accidental deletions.


4. State Extension: From Memory to Redis

Managing connections in single-machine memory is perfect in a single-instance environment. However, when your IM system needs to scale horizontally (distributed deployment), connection states become "isolated islands".

[!TIP]
Deep Solution: While handling connection competition, we synchronously maintain online states in Redis.

  • When going online: setOnlineStatus(uid, device, true)
  • When going offline: setOnlineStatus(uid, device, false)

This way, when a user on Server A sends a message to a user on Server B, the target connection can be quickly located on which machine it resides via Redis, achieving precise cross-node delivery.


5. Summary

Elegantly handling connection competition does not lie in advanced algorithms, but in respect for concurrency models:

  • The data structure must support multi-dimensional business logic (such as dual user + device indexing).
  • Resource cleanup must follow Go's concurrency philosophy, triggered automatically by Close signals.
  • Logical checks must be precise to the object level (pointer comparison), rather than relying solely on IDs.

[!NOTE]
Author's Postscript: In IM systems, connections are cheap, but state consistency is expensive. I hope this article provides you with some reference when dealing with persistent connection operations.