NashTech Blog

WebSocket Best Practices: Building Secure, Efficient Real-Time Applications While Avoiding Common Mistakes

Table of Contents

Introduction

WebSocket is a technology that has revolutionized the way we build real-time applications. In the early days, developers had to rely on constant “polling” to update data. Today, WebSocket provides a smooth, instant experience that modern users expect.
Let’s explore this technology—from fundamental concepts to best practices that every developer should know.

1. What is WebSocket?

WebSocket is a communication protocol that provides a full-duplex channel over a single TCP connection. Standardized in RFC 6455 in 2011, WebSocket was introduced to overcome the limitations of HTTP in building real-time applications.

Key Features

  • Persistent Connection: Maintains a continuous connection instead of sending a request/response each time like HTTP.
  • Full-Duplex Communication: Both client and server can send data at any time.
  • Low Latency: No handshake for every message, reducing delay.
  • Efficient: Less overhead compared to HTTP polling.
  • Protocol Flexibility: Supports both text and binary data.

WebSocket URL Format:

ws://example.com/socket    (unencrypted)
wss://example.com/socket   (encrypted with SSL/TLS – recommended for production)

2. Understanding the Differences: WebSocket, HTTP, Polling, and Server-Sent Events

CriteriaWebSocketHTTPHTTP PollingServer-Sent Events
ConnectionPersistentStatelessStatelessPersistent
CommunicationFull-duplexRequest/ResponseRequest/ResponseServer → Client
OverheadLowHighVery HighMedium
Real-Time PerformanceExcellentNonePoorGood
Implementation ComplexityHighLowLowMedium
Browser SupportIE 10+UniversalUniversalNo support in old IE/Edge
Firewall/Proxy IssuesPossibleNoneNoneRare
Resource UsageLowMediumHighMedium

3. How WebSocket Works

WebSocket Handshake Process

WebSocket begins with a special HTTP request, then upgrades to a WebSocket connection:

# Client sends upgrade request
GET /chat HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

# Server responds to accept the upgrade
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

4. Implementing WebSocket with SockJS and STOMP: A Complete Real-Time Solution

4.1 SockJS – Fallback Support

SockJS is a JavaScript library that provides fallback transport when WebSocket is not available. It automatically switches to methods such as xhr-polling, jsonp-polling, or iframe-transport.

Why SockJS is needed

  • Some corporate firewalls block WebSocket traffic.
  • Older proxy servers may not support WebSocket.
  • Network infrastructure can sometimes be unstable with persistent connections.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("https://*.mycompany.com")
                .withSockJS() // Enable SockJS fallback
                .setHeartbeatTime(25000);
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }
}

4.2 STOMP Protocol – Structured Messaging

STOMP (Simple Text Oriented Messaging Protocol) provides structured messaging, similar to HTTP headers:

@Controller
public class ChatController {

    @MessageMapping("/chat.send")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage message) {
        message.setTimestamp(Instant.now());
        return message;
    }

    // Send to a specific user
    @MessageMapping("/chat.private")
    public void sendPrivateMessage(@Payload PrivateMessage message,
                                   SimpMessageHeaderAccessor headerAccessor) {
        messagingTemplate.convertAndSendToUser(
            message.getRecipient(),
            "/queue/private",
            message
        );
    }
}

Frontend with Vue.js and STOMP

// npm install @stomp/stompjs sockjs-client

import { Client } from '@stomp/stompjs';
import SockJS from 'sockjs-client';

export default {
    data() {
        return {
            stompClient: null,
            messages: []
        };
    },

    methods: {
        connect() {
            this.stompClient = new Client({
                webSocketFactory: () => new SockJS('/ws'),
                onConnect: () => {
                    // Subscribe to public messages
                    this.stompClient.subscribe('/topic/public', (message) => {
                        this.messages.push(JSON.parse(message.body));
                    });
                }
            });
            this.stompClient.activate();
        },

        sendMessage(content) {
            this.stompClient.publish({
                destination: '/app/chat.send',
                body: JSON.stringify({ content, sender: this.username })
            });
        }
    }
};

5. Common Mistakes to Avoid When Using WebSocket

5.1 Security Vulnerabilities in Authentication/Authorization

❌ Mistake – No authentication

@Override
public boolean beforeHandshake(...) {
    return true; // ⚠️ DANGEROUS: Allows all connections!
}

Consequences

  • Unauthorized users can join chat rooms.
  • Attackers can spam or spread malware.
  • Sensitive information can be exposed.
  • The system may suffer DDoS attacks.

✅ Safe Implementation

@Override
public boolean beforeHandshake(ServerHttpRequest request,
                             ServerHttpResponse response,
                             WebSocketHandler wsHandler,
                             Map<String, Object> attributes) {

    // Extract and validate JWT token
    String token = extractJwtToken(request);
    if (!jwtTokenProvider.validateToken(token)) {
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return false;
    }

    // Check user permissions
    String userId = jwtTokenProvider.getUserIdFromToken(token);
    String roomId = extractRoomId(request);
    if (!hasRoomPermission(userId, roomId)) {
        response.setStatusCode(HttpStatus.FORBIDDEN);
        return false;
    }

    attributes.put("userId", userId);
    return true;
}

5.2 Memory Leaks and Resource Cleanup Issues

❌ Problem

Not releasing sessions when connections are closed causes WebSocketSession objects to remain in memory → leading to memory leaks, especially critical in systems with many concurrent connections.

private final Set<WebSocketSession> sessions = new HashSet<>(); // Not thread-safe!

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
    // ⚠️ Session not removed -> Memory leak!
}

✅ Proper Cleanup

Ensure sessions are removed and resources released properly when users disconnect.

private final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
    sessions.remove(session);
    cleanupUserData(getUserId(session)); // Cleanup related data
}

5.3 No Rate Limiting

❌ Problem

Without limiting messages, users can spam continuously → server overload or DDoS.

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
    // ❌ No rate limiting
    processMessage(session, message);
}

✅ Solution

Add a per-user rate limit (e.g., 10 messages per minute).

private final Map<String, RateLimiter> userRateLimiters = new ConcurrentHashMap<>();

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
    String userId = getUserId(session);

    // Rate limiting: 10 messages/minute per user
    RateLimiter limiter = userRateLimiters.computeIfAbsent(userId,
        k -> RateLimiter.create(10.0 / 60.0));

    if (!limiter.tryAcquire()) {
        sendError(session, "Rate limit exceeded");
        return;
    }

    processMessage(session, message);
}

5.4 No Message Size Limits

❌ Problem

Without size limits, attackers can send large payloads → memory exhaustion.

@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
    // ❌ No message size limit
}

✅ Solution

Configure message and buffer size limits.

@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
    registry.setMessageSizeLimit(64 * 1024); // 64KB limit
    registry.setSendBufferSizeLimit(512 * 1024); // 512KB buffer
    registry.setSendTimeLimit(20000); // 20s timeout
}

5.5 Loose CORS Configuration

❌ Dangerous

.setAllowedOrigins("*") // ⚠️ NEVER use in production!

✅ Secure

.setAllowedOriginPatterns("https://*.mycompany.com", "<https://localhost:3000>")

6. Important Best Practices

6.1 Connection Management

  • Heartbeat/Ping-Pong: Detect dead connections.
  • Graceful Shutdown: Close connections properly when restarting the server.
  • Connection Pooling: Group connections by rooms/topics for efficient broadcasting.
  • Session Cleanup: Always remove sessions and related data when disconnecting.

6.2 Error Handling and Resilience

class ResilientWebSocket {
    constructor(url) {
        this.url = url;
        this.messageQueue = []; // Queue messages when disconnected
        this.connect();
    }

    send(message) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(message));
        } else {
            this.messageQueue.push(message); // Queue for later
        }
    }

    onReconnected() {
        // Send queued messages
        while (this.messageQueue.length > 0) {
            this.send(this.messageQueue.shift());
        }
    }
}

6.3 Performance Optimization

  • Message Batching: Combine multiple small messages into one batch.
  • Binary Protocol: Use binary format for large data.
  • Compression: Enable per-message-deflate extension.
  • Load Balancing: Distribute connections across multiple servers.
  • Monitoring: Track connection counts, message rates, and memory usage.

6.4 Security Best Practices

  • Input Validation: Validate all incoming messages.
  • Rate Limiting: Enforce per-user and per-IP limits.
  • HTTPS/WSS: Always use secure connections in production.
  • Token Expiration: Implement token refresh.
  • Message Encryption: Encrypt sensitive data end-to-end.

6.5 Monitoring and Debugging

@Component
public class WebSocketMetrics {

    private final Counter connectionsCounter = Counter.builder("websocket.connections")
            .description("WebSocket connections count")
            .register(Metrics.globalRegistry);

    private final Timer messageProcessingTimer = Timer.builder("websocket.message.processing")
            .description("Message processing time")
            .register(Metrics.globalRegistry);

    public void recordConnection() {
        connectionsCounter.increment();
    }

    public void recordMessageProcessing(long duration) {
        messageProcessingTimer.record(duration, TimeUnit.MILLISECONDS);
    }
}

7. Conclusion

WebSocket has become the backbone of modern real-time applications. From understanding its fundamentals to implementing secure and efficient solutions, mastering WebSocket will empower you to build scalable, reliable systems.

🎯 Key Takeaways:

  • Know when to use it: WebSocket is not a silver bullet; choose the right tool for the right job.
  • Security first: Authentication, authorization, and input validation are essential.
  • Handle failures gracefully: Networks are unreliable—be prepared for reconnection and error handling.
  • Monitor and optimize: Track performance metrics to ensure scalability.
  • Focus on user experience: Implement loading states, offline support, and message queuing.

8. References

📚 Specifications & Documentation:

🛠️ Libraries & Tools:

Picture of Loan Nguyen Thi

Loan Nguyen Thi

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top