Push Model — Lecture Breakdown
1. THE GIST
Push is the pattern where the server sends data to clients without them asking for it — the server has the knowledge, the client just needs to be connected and listening. It's the foundation of real-time systems: chat apps, live notifications, collaborative tools. The key requirement is a bidirectional protocol (WebSockets over TCP being the go-to) so the server can write to the client socket at any time. The major trade-off is that the server has no way to know if a client can handle the load it's pushing — which is exactly why Kafka chose long polling over push. Push is real-time and elegant, but it forces the client to keep up.
2. THE BREAKDOWN
What Push Actually Is
The server writes data directly to a connected client's socket without the client making a request. The only prerequisite: an open connection exists. Everything else is just writing bytes to that socket.
Normal request-response: Client → asks → Server → answers
Push: Server → writes → Client (no ask needed)
The "push" label isn't magic — it's literally the server calling socket.send() on the client's connection whenever it has something to say.
Why Request-Response Fails for Real-Time
If 100,000 users are subscribed to a YouTube channel and a new video is uploaded:
- Request-response: each of those 100,000 clients would need to repeatedly poll "any new videos?" — 100,000 × N requests per minute of useless network traffic
- Push: server detects the upload, writes one notification to each connected socket
But YouTube doesn't actually maintain 100M open connections. They push to Apple APNs / Google FCM (Firebase Cloud Messaging), which have the infrastructure to fan those notifications out to mobile devices. Direct push at 100M connections is impractical — even YouTube uses an intermediary.
The Bidirectional Protocol Requirement
Push requires a connection where the server can initiate a write. This rules out plain HTTP/1.1 (server can only respond, not initiate). Options that work:
- WebSockets — full bidirectional, most common for browser push
- gRPC server-streaming — server sends a stream of messages in response to one client request
- TCP directly — raw bidirectional, what WebSockets are built on
- RabbitMQ — push model for message queues; broker pushes messages to consumers as they arrive
RabbitMQ Push vs Kafka Long-Poll
This is the critical architectural distinction:
RabbitMQ (push):
- Broker sees a message → immediately pushes to connected consumers
- Consumers must keep up with the rate of incoming messages
- Fast delivery, but consumer can be overwhelmed
Kafka (long poll/pull):
- Consumer asks "any messages?" — Kafka holds connection until messages arrive
- Consumer controls the pace — reads when ready
- Slower max throughput but zero risk of overwhelming consumers
The choice comes down to: who controls flow? Push = server controls. Pull = client controls.
The Scalability Wall of Push
Three hard problems with push at scale:
- Client must be online — if the client disconnects, the push goes nowhere. You need buffering or a fallback (store → deliver on reconnect)
- Client can be overwhelmed — server has no visibility into whether the client is processing fast enough. TCP flow control helps at the transport layer but doesn't solve application-level backpressure
- Connection management at scale — 1M persistent WebSocket connections = 1M file descriptors, memory for each connection state, and CPU cost to fan out to all of them
The Connection Cleanup Problem (From the Demo)
Classic bug in push systems: a client disconnects but the server still has its socket in the active connections list. Next time you loop to broadcast, you write to a dead socket → error or silent failure.
The fix: check socket.readyState before sending, and clean up closed connections from your list. In production, wire the close event to remove from the registry.
3. ENGINEER'S NOTEBOOK
Core idea: Server writes to client sockets proactively. Client just needs to be connected and listening. Server has knowledge; client passively receives it.
Key moving parts:
- Open connection — prerequisite for push; typically WebSocket, gRPC stream, or raw TCP
- Connection registry — server maintains a list/map of active client connections to fan out to
- Broadcast loop — server iterates connections and calls
send()on each - Readiness check — before sending, verify socket is still open (
OPENstate) closeevent handler — removes disconnected clients from registry; missing this = memory leak + errors- APNs / FCM — Apple/Google push infrastructure; used when you can't maintain direct connections at scale (mobile push)
Gotchas:
- No backpressure awareness — push doesn't know if the client is slow. A fast producer + slow consumer = client buffer fills → connection drops or data loss. Build acknowledgment or rate limiting on top if this matters
- Connection leak — the most common bug. Client disconnects, server doesn't clean up, registry grows forever, broadcast loop gets slower and slower, eventually crashes
- Fan-out cost scales linearly — broadcasting to N clients costs O(N). At 10K concurrent users, every single message triggers 10K socket writes. Optimize with rooms/topics (Socket.io's model) so you only fan out to relevant subscribers
- WebSocket connection limits — each WebSocket is a file descriptor. Linux's default
ulimitis often 1024 open files per process. In production: setulimit -n 1000000and configure your OS accordingly - Sticky load balancing required — WebSocket connections are stateful (persistent). If you have multiple server instances, a client connected to instance A can't receive pushes from instance B. Solution: use Redis Pub/Sub or a message broker as the fan-out layer so all instances can broadcast to all clients
Terms worth knowing:
- Push — server-initiated data delivery to a connected client without a client request
- WebSocket — bidirectional persistent TCP connection with a lightweight framing protocol on top; established via HTTP upgrade handshake
- Connection Registry — server-side data structure (Map/Set) tracking all active client connections for broadcasting
- Fan-out — sending one message to many recipients; the core operation in push systems
- Backpressure — mechanism for a slow consumer to signal to a fast producer to slow down; largely absent in naive push implementations
- APNs / FCM — Apple Push Notification Service / Firebase Cloud Messaging; managed infrastructure for pushing to mobile devices at scale
readyState— WebSocket property:CONNECTING(0),OPEN(1),CLOSING(2),CLOSED(3); always check=== 1before sending- WebSocket Upgrade — HTTP request with
Upgrade: websocketheader that converts an HTTP connection into a persistent WebSocket connection
Don't forget this:
- Always wire
ws.on('close')to remove dead connections — missing this is the #1 push server bug - Check
ws.readyState === WebSocket.OPENbefore everysend()in a broadcast loop - Push requires sticky sessions or a shared pub/sub layer (Redis) when running multiple server instances
- For mobile/offline clients: push to APNs/FCM, not direct WebSocket — they handle the delivery guarantees
- Push = server controls flow. Pull = client controls flow. Choose based on who can absorb the load
4. QUICK REFERENCE CARD
PUSH MODEL — QUICK REF
─────────────────────────────────────────────────
Pattern: Server → writes to client socket (no client request needed)
Requires: persistent bidirectional connection (WebSocket, gRPC stream, TCP)
WebSocket states (readyState):
0 = CONNECTING 1 = OPEN 2 = CLOSING 3 = CLOSED
Always check === 1 before send()
Push vs Pull:
Push (RabbitMQ) → broker pushes on arrival, client must keep up
Pull (Kafka) → client requests when ready, controls its own pace
Production push checklist:
✓ Handle ws.on('close') → remove from registry
✓ Check readyState before every broadcast send()
✓ Use Map<clientId, ws> not Array (O(1) lookup + delete)
✓ Use Redis Pub/Sub for multi-instance fan-out
✓ Set ulimit -n high (default 1024 is too low)
✓ Implement heartbeat / ping-pong to detect zombie connections
When NOT to use push:
✗ Clients that connect/disconnect frequently (polling better)
✗ Very high-volume streams to slow consumers (use pull/long poll)
✗ Mobile offline clients (use APNs/FCM instead)
─────────────────────────────────────────────────
5. THE "AHA" MOMENT
The Breaking News Alert vs The Newspaper
The pull model (polling) is like buying a newspaper. Every morning you walk to the shop and ask "any news?" The shop answers and you walk home. Tomorrow you do it again. Nothing happens between your visits even if the world is on fire.
Push is the emergency broadcast alert that blares on every phone simultaneously the moment something critical happens. You didn't ask for it. You didn't check anything. The system just wrote directly to every connected device the instant the event occurred.
Now here's the real engineer's insight from this lecture: imagine if that emergency alert system had to blast a message to 100 million phones and each phone had to reply "got it" before the next message could go out. That's synchronous push — it'd take hours. Instead, it just fires and forgets to every device.
The RabbitMQ vs Kafka distinction is the same: RabbitMQ is the emergency alert blasting to everyone as fast as it can. Kafka is the news app where you choose to refresh. If you're a powerful server, take the push. If you're a slower consumer, take the pull — you read at your own pace.
The connection cleanup bug is like an emergency alert system that still has 10 million deactivated phone numbers in its list. Every broadcast wastes time trying to reach dead phones. The ws.on('close') handler is the "unsubscribe" button — without it, your list only ever grows.
6. SHOW ME THE CODE
// ─────────────────────────────────────────────────────
// PUSH MODEL — WebSocket Chat Server (Production-Grade)
// npm install ws uuid
//
// Fixes all the bugs from the lecture demo:
// - Connection cleanup on disconnect
// - readyState check before send
// - Map instead of Array for O(1) operations
// - Heartbeat to detect zombie connections
// ─────────────────────────────────────────────────────
const http = require("http");
const WebSocket = require("ws");
const { v4: uuidv4 } = require("uuid");
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// ── Connection Registry ───────────────────────────────
// WHY Map over Array: O(1) lookup and delete by clientId
// Array requires O(N) filter on disconnect — bad at scale
const clients = new Map(); // clientId → { ws, username, connectedAt }
// ── Broadcast helper ──────────────────────────────────
// Core of the push model: write to every connected socket
function broadcast(message, excludeClientId = null) {
const payload = JSON.stringify(message);
let successCount = 0;
let failCount = 0;
clients.forEach((client, clientId) => {
if (clientId === excludeClientId) return; // don't echo to sender (optional)
// CRITICAL: always check readyState before sending
// WHY: a client can disconnect between iterations of this loop
// Sending to a closed socket throws an error without this check
if (client.ws.readyState === WebSocket.OPEN) {
client.ws.send(payload);
successCount++;
} else {
// Found a zombie connection — clean it up proactively
clients.delete(clientId);
failCount++;
}
});
console.log(`[Broadcast] Sent to ${successCount} clients, cleaned ${failCount} zombies`);
}
// ── Connection handler ────────────────────────────────
wss.on("connection", (ws, req) => {
// Generate a stable ID for this client
// WHY not use remote port: ports can be reused after disconnect
const clientId = uuidv4();
const username = `User_${clientId.slice(0, 6)}`;
// Register in our connection map
clients.set(clientId, { ws, username, connectedAt: Date.now() });
console.log(`[+] ${username} connected | Total: ${clients.size}`);
// Push a welcome message to JUST this new client (targeted push)
ws.send(JSON.stringify({
type: "welcome",
message: `Welcome! You are ${username}`,
onlineCount: clients.size
}));
// Push a join notification to ALL OTHER clients
// This is the server-initiated push — no client requested this
broadcast({
type: "user_joined",
username,
message: `${username} joined the chat`,
onlineCount: clients.size
}, clientId); // exclude the joining client
// ── Incoming message handler ──────────────────────
ws.on("message", (rawMessage) => {
let parsed;
try {
parsed = JSON.parse(rawMessage.toString());
} catch {
ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" }));
return;
}
console.log(`[Message] ${username}: ${parsed.text}`);
// PUSH: broadcast to all connected clients
// Server received one message → pushes to N clients
// This is the fan-out operation — O(N) where N = connected clients
broadcast({
type: "message",
from: username,
text: parsed.text,
timestamp: Date.now()
});
});
// ── Disconnect handler — THE MOST IMPORTANT PART ──
// WHY: without this, dead connections accumulate in our Map
// Over time: broadcast loop gets slower, memory leaks, server degrades
ws.on("close", (code, reason) => {
clients.delete(clientId);
console.log(`[-] ${username} disconnected (code: ${code}) | Total: ${clients.size}`);
// Push departure notification to remaining clients
broadcast({
type: "user_left",
username,
message: `${username} left the chat`,
onlineCount: clients.size
});
});
ws.on("error", (err) => {
console.error(`[Error] ${username}:`, err.message);
clients.delete(clientId); // clean up on error too
});
});
// ── Heartbeat — detect zombie connections ─────────────
// WHY: clients can disconnect without sending a close frame
// (network drop, browser crash, etc.) — the server doesn't know
// Without this: Map fills with dead connections that never clean up
const HEARTBEAT_INTERVAL = 30_000; // 30s
setInterval(() => {
console.log(`[Heartbeat] Checking ${clients.size} connections`);
clients.forEach((client, clientId) => {
if (!client.isAlive) {
// No pong received since last ping → connection is dead
console.log(`[Heartbeat] Terminating zombie: ${client.username}`);
client.ws.terminate(); // force close
clients.delete(clientId);
return;
}
// Reset flag and send ping — client must respond with pong
client.isAlive = false;
client.ws.ping(); // WebSocket protocol-level ping
});
}, HEARTBEAT_INTERVAL);
// Wire pong handler when registering connections
wss.on("connection", (ws) => {
ws.isAlive = true;
ws.on("pong", () => {
ws.isAlive = true; // client is still alive
});
});
server.listen(8080, () => {
console.log("WebSocket push server on :8080");
});
// ─────────────────────────────────────────────────────
// TEST IN BROWSER CONSOLE:
//
// const ws = new WebSocket('ws://localhost:8080');
// ws.onmessage = e => console.log(JSON.parse(e.data));
// ws.send(JSON.stringify({ text: "Hello everyone!" }));
//
// Open 3 browser tabs → each gets push notifications when
// any other user sends a message. Nobody is polling.
// Server drives all the communication.
// ─────────────────────────────────────────────────────
7. WHERE YOU'LL SEE THIS IN THE WILD
Slack / Discord / WhatsApp Web — Every message you receive without refreshing is a server push over a persistent WebSocket. Your client connects once on login. The server maintains your connection in a registry. When someone sends you a message, the server pushes it to your socket. This is why Slack feels instantaneous but consumes a persistent TCP connection the whole time you're logged in.
RabbitMQ Consumer Push — When you set up a RabbitMQ consumer with channel.consume(), you're opting into push mode. The broker pushes messages to your consumer as they arrive in the queue. The prefetch setting is your one lever for backpressure — it limits how many unacknowledged messages the broker will push at once before waiting.
gRPC Server-Side Streaming — One client request, N server responses pushed over time. Used heavily in internal microservice communication: a client subscribes to a stream of order updates, price feeds, or sensor readings and receives pushes as events occur. Google uses this pattern extensively in their internal infrastructure.
Multiplayer Gaming (real-time state sync) — Every game state update (player position, health, events) is a server push to all players in the session. At 60Hz game tick rate, the server is pushing 60 updates per second to every connected player. This is why game servers are so carefully engineered around connection management and CPU cost per broadcast.
Financial Market Data Feeds — Bloomberg, Reuters, stock exchange feeds. The moment a trade executes, the price update is pushed to every subscribed client simultaneously. Microsecond latency matters here — polling would be completely impractical. These systems often use raw TCP or custom binary protocols instead of WebSockets for lower overhead.
Socket.io with Redis Adapter (Multi-Server Push) — The production solution to the sticky-session problem. Each app server instance connects to a shared Redis channel. When any instance receives a message, it publishes to Redis. Redis pushes to all other instances. Each instance then pushes to its locally connected clients. The result: true fan-out across all server instances, regardless of which one the client connected to.
Mobile Push (APNs / FCM) — The infrastructure layer that makes push practical at billions of devices. Your app server sends one HTTP request to Apple/Google with a notification payload. APNs/FCM handles maintaining persistent connections to every iPhone/Android globally, managing offline delivery queuing, and the actual push to the device. You get push semantics without managing 1B WebSocket connections.
8. EXPLAIN IT TO MY NON-TECH FRIEND
Imagine two ways a news station could get you information.
The old way (polling): You call the news hotline every hour and ask "anything new?" Sometimes they say yes, sometimes no. You're doing all the work. The station doesn't reach out to you — you have to keep calling. Multiply this by a million listeners calling every hour and the phone lines are constantly jammed with "anything new? ... anything new? ... anything new?"
The push way: You give the news station your phone number once. Now whenever breaking news happens — a fire, an election result, a traffic accident — they call you. You didn't ask. You didn't check. They just ring your phone the moment they have something worth telling you.
That's push. The server has your "phone number" (your open connection), and it calls you whenever something happens.
The catch is obvious if you think about it: you have to have your phone on and with you. If you're in the subway with no signal — missed call. If you turn your phone off — they can't reach you. And if the news station tries to call a million people simultaneously the moment something happens, their phone system has to be built to handle that load.
This is exactly why YouTube doesn't push notifications directly from their servers to 100 million subscribers. They tell Apple and Google — who already have a permanent line to every iPhone and Android on earth — and they do the calling. YouTube just hands off the message and says "deliver this."
The chat demo is the simplest version: you connect once, and from that moment, every message anyone types gets pushed straight to your screen. No refreshing. No asking. The server just... tells you.