TL;DR
WebSocket provides a persistent, bidirectional channel over a single TCP connection — unlike HTTP which is request-response. Use ws:// or wss:// (secure) with the native browserWebSocket API, the ws library for Node.js servers, or Socket.io when you need rooms, reconnection, and fallbacks. Authenticate via JWT in the first message or query param. Scale horizontally with sticky sessions + Redis Pub/Sub. Implement exponential backoff reconnection for robustness. Use our online WebSocket tester to debug connections instantly.
WebSocket vs HTTP — Persistent Bidirectional Connection Explained
HTTP was designed for the document web: a client asks, a server answers, and the conversation ends. Each request is independent and stateless. This model works well for fetching pages, loading assets, or calling REST APIs — but it breaks down when the server needs to push data to clients without being asked first.
WebSocket solves this with a persistent connection. After an HTTP upgrade handshake, the TCP connection stays open and becomes a bidirectional channel. Both sides can send frames at any time with extremely low overhead (as little as 2 bytes of framing overhead per message vs hundreds of bytes of HTTP headers).
| Feature | HTTP/1.1 | WebSocket |
|---|---|---|
| Connection | Short-lived (keep-alive is optional) | Persistent until closed |
| Direction | Client initiates (request-response) | Full-duplex (either side can send) |
| Overhead per message | ~500–1000 bytes (headers) | 2–10 bytes (framing) |
| Server push | Polling or SSE only | Native, zero-latency |
| Protocol | http:// / https:// | ws:// / wss:// |
| Best use cases | CRUD APIs, file downloads, page loads | Chat, live feeds, gaming, collaboration |
The upgrade handshake is critical to understand. The client sends a normal HTTP GET with special headers:
# Client sends HTTP Upgrade request:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
# Server responds with 101 Switching Protocols:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
# From this point, the TCP connection carries WebSocket frames — not HTTPUse wss:// (WebSocket Secure) in production — it tunnels WebSocket over TLS exactly like HTTPS does for HTTP. Plain ws:// should only be used in local development.
Browser WebSocket API — onopen, onmessage, readyState, send()
Every modern browser ships with a native WebSocket API. No libraries required for the client side.
// 1. Open a connection
const ws = new WebSocket('wss://example.com/ws');
// 2. Connection established
ws.onopen = () => {
console.log('Connected, readyState:', ws.readyState); // 1 = OPEN
ws.send('Hello server!');
// Send JSON
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
};
// 3. Receive messages from server
ws.onmessage = (event) => {
const data = event.data; // string or Blob (binary)
const msg = JSON.parse(data); // parse JSON messages
console.log('Received:', msg);
};
// 4. Connection closed
ws.onclose = (event) => {
console.log('Closed:', event.code, event.reason);
// code 1000 = normal, 1006 = abnormal (network failure)
};
// 5. Error
ws.onerror = (error) => {
console.error('WebSocket error:', error);
// Note: always followed by onclose
};
// 6. readyState values
// ws.CONNECTING = 0 (upgrade in progress)
// ws.OPEN = 1 (ready to send/receive)
// ws.CLOSING = 2 (close handshake in progress)
// ws.CLOSED = 3 (connection closed)
// 7. Close gracefully
ws.close(1000, 'User logged out'); // code + optional reason string
// 8. Binary data
ws.binaryType = 'arraybuffer'; // or 'blob' (default)
ws.onmessage = (event) => {
const buffer = event.data; // ArrayBuffer
const view = new Uint8Array(buffer);
};Always check ws.readyState === WebSocket.OPEN before calling ws.send() — sending on a closed or connecting socket throws an error. Buffer messages in a queue and flush them in onopen if needed.
Node.js ws Library — WebSocketServer, Broadcast, Heartbeat
The ws npm package is the most popular, lightweight WebSocket server for Node.js. It is a thin wrapper over the native WebSocket protocol with no opinion on your architecture.
npm install ws
npm install --save-dev @types/ws # TypeScript typesimport { WebSocketServer, WebSocket } from 'ws';
import http from 'http';
const server = http.createServer();
const wss = new WebSocketServer({ server });
// Broadcast helper — send to all connected clients
function broadcast(wss: WebSocketServer, data: string) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
console.log('Client connected from', ip);
ws.on('message', (rawData) => {
const message = rawData.toString();
console.log('Received:', message);
// Echo to sender
ws.send(JSON.stringify({ type: 'echo', data: message }));
// Broadcast to all clients
broadcast(wss, JSON.stringify({ type: 'broadcast', from: ip, data: message }));
});
ws.on('close', (code, reason) => {
console.log('Client disconnected:', code, reason.toString());
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
// Send welcome message
ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
});
// Ping/pong heartbeat — detect dead connections
const HEARTBEAT_INTERVAL = 30_000; // 30 seconds
wss.on('connection', (ws) => {
(ws as any).isAlive = true;
ws.on('pong', () => { (ws as any).isAlive = true; });
});
setInterval(() => {
wss.clients.forEach((ws) => {
if ((ws as any).isAlive === false) {
ws.terminate(); // Connection is dead
return;
}
(ws as any).isAlive = false;
ws.ping(); // Send ping frame — client auto-responds with pong
});
}, HEARTBEAT_INTERVAL);
server.listen(8080, () => console.log('WS server on ws://localhost:8080'));The ping/pong heartbeat pattern is essential. Without it, connections that drop silently (e.g., a mobile device going offline) will stay in wss.clients forever, leaking memory. The server pings every 30 seconds; if no pong is received, the connection is terminated.
Socket.io — Rooms, Namespaces, Emit/On, Acknowledgments, Redis Adapter
Socket.io is a feature-rich real-time library that provides rooms, namespaces, automatic reconnection, broadcasting, and a Redis adapter for horizontal scaling.
npm install socket.io # server
npm install socket.io-client # client// --- SERVER (Node.js) ---
import { Server } from 'socket.io';
import http from 'http';
const httpServer = http.createServer();
const io = new Server(httpServer, {
cors: { origin: 'https://yourapp.com', credentials: true },
});
io.on('connection', (socket) => {
console.log('Connected:', socket.id);
// Join a room
socket.on('join-room', (roomId: string) => {
socket.join(roomId);
io.to(roomId).emit('user-joined', { userId: socket.id });
});
// Custom event
socket.on('chat-message', (msg: string) => {
// Broadcast to everyone in the room EXCEPT sender
socket.to('room-1').emit('chat-message', { from: socket.id, msg });
});
// Acknowledgment (callback)
socket.on('save-data', async (data, callback) => {
try {
await db.save(data);
callback({ status: 'ok' }); // success ack
} catch (err) {
callback({ status: 'error', message: (err as Error).message });
}
});
socket.on('disconnect', () => {
console.log('Disconnected:', socket.id);
});
});
// Namespace — separate channel for admin
const adminNsp = io.of('/admin');
adminNsp.on('connection', (socket) => {
socket.emit('admin-data', { stats: 'secret' });
});
httpServer.listen(3000);
// --- CLIENT ---
import { io } from 'socket.io-client';
const socket = io('https://yourapp.com', {
auth: { token: 'jwt-here' }, // auth token
reconnectionDelayMax: 10000, // max 10s between retries
});
socket.emit('join-room', 'room-42');
socket.on('chat-message', (data) => {
console.log(data.from, ':', data.msg);
});
// Acknowledgment
socket.emit('save-data', { key: 'value' }, (response) => {
if (response.status === 'ok') console.log('Saved!');
});For scaling across multiple processes, use the Redis adapter:
npm install @socket.io/redis-adapter ioredis
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'ioredis';
const pubClient = createClient({ host: 'redis-host', port: 6379 });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// Now events are synchronized across all Socket.io server instancesReact useWebSocket Hook — Reconnect Logic, Message History, Lazy Connect
The react-use-websocket library provides a batteries-included hook for WebSocket connections in React applications, handling reconnection, message queuing, and state management automatically.
npm install react-use-websocketimport useWebSocket, { ReadyState } from 'react-use-websocket';
interface ChatMessage {
from: string;
text: string;
}
export function ChatComponent() {
const [input, setInput] = useState('');
const {
sendMessage,
sendJsonMessage,
lastMessage,
lastJsonMessage,
readyState,
getWebSocket,
} = useWebSocket('wss://example.com/ws', {
// Automatic reconnection with backoff
shouldReconnect: (closeEvent) => closeEvent.code !== 1000,
reconnectAttempts: 10,
reconnectInterval: (attemptNumber) =>
Math.min(1000 * 2 ** attemptNumber, 30000), // exponential backoff
// Keep last 50 messages in history
queryParams: { token: 'jwt-token' }, // appended to URL
// Callbacks
onOpen: () => console.log('WebSocket connected'),
onClose: (event) => console.log('Closed:', event.code),
onError: (event) => console.error('Error:', event),
onMessage: (event) => {
const data = JSON.parse(event.data) as ChatMessage;
setMessages((prev) => [...prev, data]);
},
});
// Lazy connect — don't connect until a condition is met
const { sendMessage: lazyWs } = useWebSocket(
'wss://example.com/ws',
{ connect: isLoggedIn } // only connects when isLoggedIn = true
);
const connectionStatus = {
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Open',
[ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Closed',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated',
}[readyState];
return (
<div>
<p>Status: {connectionStatus}</p>
{lastMessage && <p>Last message: {lastMessage.data}</p>}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button
onClick={() => {
sendJsonMessage({ type: 'chat', text: input });
setInput('');
}}
disabled={readyState !== ReadyState.OPEN}
>
Send
</button>
</div>
);
}For a manual reconnect hook without any library, implement the pattern yourself using useRef and useEffect with the exponential backoff algorithm shown in the FAQ section below.
WebSocket Authentication — JWT in Query Param vs Cookie vs First Message
WebSocket connections do not support custom HTTP headers during the upgrade handshake (browsers restrict this). There are three practical authentication patterns:
Pattern 1: JWT in Query Parameter
// Client
const token = localStorage.getItem('jwt');
const ws = new WebSocket(`wss://example.com/ws?token=${token}`);
// Server (Node.js ws)
import jwt from 'jsonwebtoken';
import { parse } from 'url';
wss.on('connection', (ws, req) => {
const { query } = parse(req.url || '', true);
const token = query.token as string;
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
(ws as any).userId = (payload as any).sub;
(ws as any).authenticated = true;
} catch {
ws.close(1008, 'Invalid token'); // 1008 = Policy Violation
return;
}
});Downside: tokens in URLs appear in server access logs and browser history. Mitigate by using short-lived tokens (60-second expiry) generated specifically for WebSocket connections.
Pattern 2: First Message Authentication
// Client — send auth as first message
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: getJwt() }));
};
// Server — expect auth as first message
wss.on('connection', (ws) => {
let authenticated = false;
ws.on('message', (rawData) => {
const msg = JSON.parse(rawData.toString());
if (!authenticated) {
if (msg.type !== 'auth') {
ws.close(1008, 'Must authenticate first');
return;
}
try {
const payload = jwt.verify(msg.token, process.env.JWT_SECRET!);
(ws as any).userId = (payload as any).sub;
authenticated = true;
ws.send(JSON.stringify({ type: 'auth-ok' }));
} catch {
ws.close(1008, 'Invalid token');
}
return;
}
// Authenticated — handle message normally
handleMessage(ws, msg);
});
// Close unauthenticated connections after 5 seconds
setTimeout(() => {
if (!authenticated) ws.close(1008, 'Auth timeout');
}, 5000);
});Pattern 3: Socket.io Middleware Auth
// Client
const socket = io('https://example.com', {
auth: { token: getJwt() },
});
// Server middleware — runs before 'connection' event
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
socket.data.userId = (payload as any).sub;
next(); // allow connection
} catch (err) {
next(new Error('Unauthorized')); // reject connection
}
});
io.on('connection', (socket) => {
console.log('Authenticated user:', socket.data.userId);
});Python websockets Library — asyncio Server, Client, Broadcast
The websockets library is the standard Python choice for WebSocket servers. It integrates natively with asyncio and is production-ready with TLS support.
pip install websockets# --- SERVER ---
import asyncio
import websockets
from websockets.server import WebSocketServerProtocol
# Connected clients registry
connected: set[WebSocketServerProtocol] = set()
async def broadcast(message: str) -> None:
if connected:
await asyncio.gather(
*[ws.send(message) for ws in connected],
return_exceptions=True, # don't stop on individual errors
)
async def handler(websocket: WebSocketServerProtocol) -> None:
connected.add(websocket)
try:
await websocket.send('{"type": "welcome"}')
async for raw_message in websocket:
data = json.loads(raw_message)
print(f"Received: {data}")
if data["type"] == "broadcast":
await broadcast(raw_message)
else:
await websocket.send(json.dumps({"type": "echo", "data": data}))
except websockets.exceptions.ConnectionClosedOK:
pass # normal closure
except websockets.exceptions.ConnectionClosedError as e:
print(f"Connection error: {e.code} {e.reason}")
finally:
connected.discard(websocket)
async def main() -> None:
async with websockets.serve(handler, "0.0.0.0", 8765):
print("WebSocket server started on ws://0.0.0.0:8765")
await asyncio.Future() # run forever
asyncio.run(main())# --- CLIENT ---
import asyncio
import websockets
import json
async def client():
uri = "wss://example.com/ws"
async with websockets.connect(uri) as websocket:
# Send
await websocket.send(json.dumps({"type": "hello", "data": "world"}))
# Receive
response = await websocket.recv()
print("Response:", json.loads(response))
# Listen loop
async for message in websocket:
print("Incoming:", message)
asyncio.run(client())For FastAPI integration, use fastapi.WebSocket which supports dependency injection and middleware:
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: str):
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Echo {client_id}: {data}")
except WebSocketDisconnect:
print(f"Client {client_id} disconnected")Go gorilla/websocket — Upgrader, ReadMessage, WriteMessage, Concurrent Writes
gorilla/websocket is the most widely used WebSocket library for Go. It is mature, well-documented, and handles the low-level framing, masking, and close handshake for you.
go get github.com/gorilla/websocketpackage main
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// Validate Origin header to prevent CSWSH
origin := r.Header.Get("Origin")
return origin == "https://yourapp.com"
},
}
// Client with mutex for concurrent writes
type Client struct {
conn *websocket.Conn
mu sync.Mutex
}
func (c *Client) WriteJSON(v interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.conn.WriteJSON(v)
}
func handleWS(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrade error: %v", err)
return
}
defer conn.Close()
client := &Client{conn: conn}
// Set read deadline / pong handler for heartbeat
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
log.Println("Client closed connection")
} else {
log.Printf("Read error: %v", err)
}
return
}
log.Printf("Received (%d): %s", messageType, p)
// Echo back
if err := client.WriteJSON(map[string]string{
"type": "echo",
"data": string(p),
}); err != nil {
log.Printf("Write error: %v", err)
return
}
}
}
func main() {
http.HandleFunc("/ws", handleWS)
log.Fatal(http.ListenAndServe(":8080", nil))
}The sync.Mutex on writes is critical: gorilla/websocket connections are not safe for concurrent writes. If you have a goroutine that pings AND a goroutine that writes messages, they will race. Use a mutex or a dedicated write channel (chan []byte) that a single writer goroutine drains.
Scaling WebSockets — Sticky Sessions, Redis Pub/Sub, Horizontal Scaling
Scaling WebSockets is harder than scaling stateless HTTP APIs because connections are persistent and bound to a specific server process. A message sent to Server A cannot reach clients connected to Server B without a coordination layer.
Sticky Sessions
Configure your load balancer to always route the same client to the same server. This is the simplest approach but limits true horizontal scaling.
# Nginx upstream with ip_hash for sticky sessions
upstream websocket_backends {
ip_hash; # route by client IP
server ws1.example.com:8080;
server ws2.example.com:8080;
server ws3.example.com:8080;
}
server {
listen 443 ssl;
location /ws {
proxy_pass http://websocket_backends;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s; # keep connection alive for 1 hour
proxy_send_timeout 3600s;
}
}Redis Pub/Sub for Cross-Server Messaging
// Without Redis: Server A cannot reach Server B's clients
// With Redis: any server publishes → Redis → all servers → their clients
import { createClient } from 'redis';
const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = publisher.duplicate();
await Promise.all([publisher.connect(), subscriber.connect()]);
// When a message comes in on this server, publish to Redis
ws.on('message', async (data) => {
await publisher.publish('chat:room-1', data.toString());
});
// Subscribe: deliver Redis messages to local clients in this room
await subscriber.subscribe('chat:room-1', (message) => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
// Connection limits per server (tune based on RAM/CPU)
// A 1GB RAM Node.js server can typically handle ~10,000 idle WS connections
// Use cluster mode or worker_threads to utilize multiple CPU coresError Handling and Reconnection — Exponential Backoff, Close Codes, Graceful Shutdown
Production WebSocket clients must handle disconnections gracefully. Networks are unreliable and servers restart. A naive immediate reconnect loop can overwhelm a restarting server (thundering herd problem).
Exponential Backoff Reconnection
class ReconnectingWebSocket {
private ws: WebSocket | null = null;
private delay = 1000; // start at 1s
private maxDelay = 30000; // cap at 30s
private maxAttempts = 10;
private attempts = 0;
private shouldClose = false;
constructor(private readonly url: string) {
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.delay = 1000; // reset backoff on successful connect
this.attempts = 0;
};
this.ws.onmessage = (event) => {
this.onMessage?.(event);
};
this.ws.onclose = (event) => {
if (this.shouldClose) return; // manual close — don't reconnect
if (event.code === 1000) return; // normal close — don't reconnect
this.attempts++;
if (this.attempts > this.maxAttempts) {
console.error('Max reconnect attempts reached');
return;
}
// Exponential backoff with jitter
const jitter = Math.random() * 1000;
const nextDelay = Math.min(this.delay * 2, this.maxDelay) + jitter;
this.delay = nextDelay;
console.log(`Reconnecting in ${Math.round(nextDelay)}ms (attempt ${this.attempts})`);
setTimeout(() => this.connect(), nextDelay);
};
}
send(data: string) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.warn('WebSocket not ready, message dropped');
}
}
close() {
this.shouldClose = true;
this.ws?.close(1000, 'Client closed');
}
onMessage?: (event: MessageEvent) => void;
}Close Codes Reference
| Code | Name | Reconnect? | Meaning |
|---|---|---|---|
1000 | Normal Closure | No | Clean shutdown, both sides agreed |
1001 | Going Away | Yes | Browser tab closed or server restarting |
1006 | Abnormal Closure | Yes | No close frame — network failure or crash |
1008 | Policy Violation | No | Auth failed, rate limited, banned |
1011 | Internal Error | Yes | Unexpected server error |
1012 | Service Restart | Yes | Server intentionally restarting |
Graceful Server Shutdown
// Gracefully close all connections before process exit
process.on('SIGTERM', () => {
console.log('SIGTERM received — closing WebSocket connections');
wss.clients.forEach((ws) => {
ws.close(1012, 'Server restarting'); // 1012 = Service Restart
});
wss.close(() => {
console.log('All WS connections closed');
server.close(() => process.exit(0));
});
// Force exit after 10 seconds if clients don't close cleanly
setTimeout(() => process.exit(1), 10_000);
});Test Your WebSocket Server
Use our online WebSocket tester to connect to any ws:// or wss:// endpoint, send custom messages, and inspect frames in real time — no installation required.
Frequently Asked Questions
Does WebSocket work through firewalls and proxies?
Most firewalls allow WebSocket because the upgrade handshake looks like a regular HTTP request on port 80 or 443. However, some corporate HTTP proxies that do not understand WebSocket may strip the Upgrade header, breaking the connection. Always use wss:// on port 443 — TLS-encrypted traffic is harder for proxies to inspect and less likely to be blocked. Socket.io's HTTP long-polling fallback handles this case automatically.
Should I use WebSocket or Server-Sent Events (SSE)?
Use SSE when you only need server-to-client push (notifications, live feeds, log streaming) — it is simpler, works over regular HTTP/2, and auto-reconnects natively. Use WebSocket when you need true bidirectional communication (chat, gaming, collaborative editing where the client also sends frequent messages). SSE has a simpler implementation and better compatibility with HTTP infrastructure (CDNs, proxies), but WebSocket offers lower latency and binary frame support.
What is the maximum number of WebSocket connections a server can handle?
Each WebSocket connection is a file descriptor. Linux defaults to 1024 file descriptors per process — raise this with ulimit -n 65535 or in /etc/security/limits.conf. Memory is the real limit: an idle Node.js WebSocket connection uses roughly 40–60 KB of RAM. A 1 GB RAM server can handle approximately 15,000–25,000 idle connections. Active connections with large message queues use significantly more. Use connection pooling and evict idle connections with heartbeat timeouts.
Key Takeaways
- Use wss:// in production: Always use WebSocket Secure (TLS) — plain ws:// exposes data and breaks on many proxies.
- Implement heartbeats: Ping/pong every 30 seconds detects silent connection drops and frees dead socket handles.
- Exponential backoff on reconnect: Never reconnect immediately — use doubling delays with jitter to avoid thundering herd.
- Socket.io for features, ws for performance: Socket.io adds rooms/namespaces/acks; raw ws is leaner for high-connection scenarios.
- Authenticate on connection or first message: Validate JWT before allowing any application-level messages.
- Mutex for Go writes: gorilla/websocket is not concurrent-write-safe — always lock before writing.
- Sticky sessions + Redis Pub/Sub is the standard horizontal scaling pattern for WebSocket clusters.
- Close code 1006 means network failure: Always reconnect on 1006; only skip reconnect on 1000 (normal) and 1008 (policy violation).
- Raise OS file descriptor limits:
ulimit -n 65535is required for high-connection servers. - Graceful shutdown: Send close code 1012 and wait for client acknowledgment before killing the process.