DEV Community

Cover image for The Compression Layer Nobody Thinks About Until It's Too Late
turboline-ai
turboline-ai

Posted on

The Compression Layer Nobody Thinks About Until It's Too Late

WebSocket optimization tends to follow a predictable pattern. You build the connection layer, get events flowing, ship the feature, and move on. Compression is an afterthought — something you bolt on later, or maybe delegate to the transport layer and forget about entirely.

Discord's engineering team learned this lesson at a scale most of us will never encounter, and the details of what they did are worth paying attention to even if your user count has fewer zeros.

The Problem With "Good Enough" Compression

When Discord migrated their gateway compression from zlib to zstandard, they cut WebSocket traffic by 40%. That is not a rounding error. That is nearly half your bandwidth costs, half your mobile data consumption, and a meaningful reduction in latency for clients on constrained connections.

Zlib has been the default choice for so long that most developers treat it like gravity — invisible, assumed, and not worth questioning. Zstandard (zstd) offers significantly better compression ratios at comparable or faster speeds, particularly on repetitive structured data like JSON event payloads. For a platform pushing millions of concurrent gateway connections, switching algorithms is not a trivial migration. Discord spent six months on it. That timeline tells you something: compression strategy is real engineering work, not a config flag you flip on a Tuesday afternoon.

Real-Time Systems Are a Different Animal

The original observation that sparked this — the idea that Discord might just be a Tumblr alternative — is actually a useful contrast to pull apart.

Tumblr is an async content platform. You post, someone reads it later, the infrastructure serves cached content to a feed. The latency requirements are loose. The data model is relatively static. You can afford to think in requests and responses.

Discord is fundamentally event-driven. Presence updates, typing indicators, message delivery, voice state changes — these are live, stateful, and time-sensitive. The infrastructure cannot think in terms of request-response cycles. It has to think in terms of persistent connections, fan-out, and continuous state synchronization across millions of clients simultaneously.

These two models require completely different architectural instincts. What works for one actively fails for the other. Treating a real-time system like a slightly faster async system is where most performance problems originate.

Where Compression Gets Complicated in Practice

The challenge with WebSocket compression is not just picking a better algorithm. It is that compression interacts with everything downstream.

Consider the per-message versus context takeover distinction in permessage-deflate. If you compress each message independently, you lose the compression efficiency that comes from shared context across messages. If you maintain context, you gain efficiency but introduce state that has to be managed carefully on both ends, especially across reconnects.

Here is a simplified example of what configuring this looks like in Node.js with the ws library:

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({
  port: 8080,
  perMessageDeflate: {
    zlibDeflateOptions: {
      level: 6,
    },
    zlibInflateOptions: {
      chunkSize: 16 * 1024,
    },
    clientNoContextTakeover: false,
    serverNoContextTakeover: false,
    threshold: 128,
  },
});
Enter fullscreen mode Exit fullscreen mode

Setting clientNoContextTakeover and serverNoContextTakeover to false tells both sides to maintain compression context across messages. For high-frequency event streams with similar payload structures, this can meaningfully improve your compression ratio. The tradeoff is memory pressure per connection, which at scale becomes a real cost you have to model.

Zstd adds another dimension here. Its dictionary compression mode lets you pre-train a compression dictionary on representative payloads, which gives you strong compression even on shorter messages where context-based approaches struggle. For a platform with predictable event schema shapes, this is where the real gains live.

Mobile Is the Forcing Function

Discord specifically called out mobile clients as a key driver for this work. That framing matters.

Mobile networks are variable, expensive for users on metered data plans, and battery-constrained. Every byte you cut from your payload is a concrete improvement to the experience for a meaningful portion of your user base. When you are operating at Discord's scale, the aggregate impact is significant enough to justify months of engineering effort.

For developers building real-time systems at smaller scales, the lesson is not to wait until you have Discord's traffic to take this seriously. The architectural decisions you make early, including your compression strategy, tend to calcify. Changing them later requires exactly the kind of sustained migration effort Discord just completed.

The Ongoing Discipline

This is what I think gets missed in discussions about WebSocket performance: it is not a box you check. Discord's compression migration was not a one-time optimization. It represents a broader engineering posture — treating the transport layer as something worth continuously measuring, questioning, and improving.

The same mindset applies to anyone building systems where low-latency data delivery is a core product requirement. At Turboline, for instance, the challenge of delivering live data at scale runs into these exact tradeoffs: compression algorithm selection, connection state management, and payload efficiency are all levers that compound over time.

The developers who build the most reliable real-time systems are the ones who treat the connection layer with the same rigor they bring to their data models and application logic. Compression is not infrastructure plumbing you configure once and ignore. It is a continuous engineering discipline, and Discord's six-month investment is the clearest proof point I have seen recently that the teams building at the frontier understand this.

Top comments (1)

Collapse
 
swapnoneel123 profile image
Swapnoneel Saha

the memory per connection tradeoff is the part teams can miss when they focus on ratio alone. i would benchmark message size, cpu time, memory per connection, reconnect behavior, and battery or radio time on mobile. keep cache context scoped to an authenticated connection and clear it on close, so state does not cross users or sessions. a staged rollout with a no compression fallback can make the migration safer.