Synero for Developers
GPTClaudeGeminiGrok

Debug, architect and build with up to four AI models

Technical decisions are too important for a single AI opinion. Synero asks up to four leading models your code, architecture, or design question independently, shows where they agree and where they split, then writes one complete consensus answer.

Real example

See four answers become one

An example of how Synero answers this kind of question: four independent answers, then one combined answer. In the app you pick 1 to 4 of 20 models, and the Agreement panel shows where they agree and where they split.

I need to design a real-time notification system that handles 100K concurrent WebSocket connections. Compare architectural approaches and their trade-offs.
Answer 1

Certainly. Designing a real-time notification system capable of supporting 100,000 concurrent WebSocket connections is a significant engineering challenge. Let's systematically analyze the architectural approaches and trade-offs.


1. Key Requirements

  • High Concurrency: 100,000+ persistent WebSocket connections
  • Scalability: Ability to scale horizontally as demand grows
  • Low Latency: Real-time message delivery
  • Reliability: Fault tolerance, connection management
  • Maintainability: Clean codebase, ease of development

2. Architectural Approaches

Approach A: Monolithic Scalable WebSocket Server

Description

Deploy a single (potentially large) server or a tightly coupled cluster managing all connections and message routing.

Components
  • WebSocket server (e.g., written in Node.js with ws, or Java with Netty)
  • Direct connection between app and clients
  • In-memory message routing/broadcast logic
Pros
  • Simplicity: Easier to build, test, deploy
  • Low Overhead: No cross-node messaging
Cons
  • Limited Scalability: Single server (even large) will struggle with 100K connections, subject to OS/network limits
  • Fault Tolerance: Process/server failure drops all connections
  • Resource Bottleneck: CPU, memory, network bandwidth limits
When to Use
  • Small scale, prototyping, or when connection count is much lower

Approach B: Horizontally Scaled WebSocket Servers + Message Broker (Pub/Sub)

Description

Deploy multiple stateless WebSocket servers behind a load balancer, using a distributed message broker for inter-node communication.

Components
  • WebSocket Gateway Layer: Fleet of stateless servers (e.g., Node.js, Go, Elixir with Phoenix/Channels)
  • Load Balancer: Distributes incoming WebSocket connections
  • Message Broker: Redis Pub/Sub, NATS, RabbitMQ, Kafka for routing messages between servers
  • Microservices: Backend services to generate/send notifications
Message Flow
  1. Client connects to one WebSocket gateway.
  2. Notification triggers message in broker.
  3. All WebSocket servers subscribe; relay to connected clients.
Pros
  • Horizontal Scalability: Easily scale the gateway fleet to handle more connections
  • Fault Isolation: Single server failure affects only a fraction of users
  • Operational Flexibility: Upgrade/maintain fleet incrementally
Cons
  • Broker Bottleneck: Redis, for example, may become a throughput limit. Cross-server broadcast traffic increases with scale ("N servers × M events").
  • Complexity: Requires careful handling of pub/sub fanout and connection state
When to Use
  • Standard approach for medium to high volumes; industry best practice (see Slack, WhatsApp, etc.)

Approach C: Cloud-Native or Serverless Solutions (Managed WebSocket Services)

Description

Leverage managed WebSocket infrastructure from cloud providers.

Examples
  • AWS API Gateway (WebSocket support)
  • AWS AppSync, AWS IoT Core
  • Azure Web PubSub Service
  • Google Cloud WebSocket offerings
Pros
  • Offloaded Ops: No need to manage cluster, scaling, failover
  • Rapid Development: Integration with other managed services
  • Auto-Scaling: Dynamically handles fluctuating load
Cons
  • Cost: Can become expensive at high connection counts
  • Vendor Lock-in: Tightly coupled to the provider’s ecosystem
  • Flexibility Limits: May not suit very custom protocols or special routing logic
When to Use
  • Teams wishing to avoid infrastructure management or for unpredictable load patterns

Approach D: Distributed Stateful WebSocket Gateways (Actor Model)

Description

Use an actor-based architecture where each user’s connection is managed by an actor; system handles automatic sharding, failover.

Example
  • Elixir/Phoenix Channels (with PubSub)
  • Akka (Scala/Java actor framework)
  • Orleans (Microsoft’s virtual actor model)
Pros
  • Natural State Management: Per-connection actors simplify user state and message routing
  • Fault Tolerance: Actors recover transparently
  • Elasticity: Guided sharding/placement for scaling
Cons
  • Learning Curve: Requires expertise in actor model frameworks
  • Debugging Complexity: Distributed actor debugging can be non-trivial
  • Still Needs Pub/Sub: For global events, you may still need a separate broker
When to Use
  • Teams with actor model expertise, or where complex per-connection state logic is needed

3. Comparative Summary Table

CriteriaMonolithic ServerScaled WS + BrokerCloud-NativeActor Model
ScalabilityLowHighVery HighHigh
Operational LoadLow (at first)ModerateVery LowModerate
CostLow (small scale)ModerateHigh (>50K conns)Moderate
ComplexityLowModerateLowHigh
CustomizabilityHighHighLow-MediumHigh
Fault IsolationPoorGoodExcellentGood

4. Recommendations & Patterns

  1. <100K Connections, Predictable Load: Scaled WebSocket Servers with Broker are the standard. Use battle-tested libraries and a fast Pub/Sub (e.g., Redis, NATS, or Kafka for large fanout).
  2. >100K or Spiky, Unpredictable Loads: Consider Cloud-Native/Managed services if budget allows, else invest in robust broker selection and sharding.
  3. Complex State Per User/Actor Model Expertise: Actor-based frameworks can simplify per-user logic, but prepare for extra complexity.
  4. Optimization Areas:
    • Ensure connection drain and recovery strategies.
    • Plan for broker scaling/partitioning to avoid bottlenecks.
    • Monitor and autoscale based on resource utilization (connections/server, CPU, memory).

5. Concrete Example Deployments

  • Slack: Uses sharded WebSocket gateways and an event broker.
  • WhatsApp: Erlang/BEAM actor-system for millions of persistent connections.
  • AWS AppSync: Managed GraphQL subscriptions over WebSockets.

In summary:
The typical and most practical approach for your requirements is Horizontally Scaled WebSocket Gateways with a Message Broker, provided you anticipate the need to scale both the connections and the notification throughput. Consider cloud-native offerings for time-to-market or where devops overhead is a concern, and actor-based systems for advanced use cases.

Would you like a detailed example architecture diagram or sample stack recommendations for implementation?

Answer 2

Real-Time Notification System: Architectural Analysis

Let me work through this systematically, because the right answer depends heavily on constraints you haven't specified, and acknowledging that uncertainty is important.

First, Clarify What "100K Concurrent" Actually Means

Before choosing architecture, these distinctions matter enormously:

code
100K concurrent connections ≠ 100K messages/second

Questions that change the answer:
├── Message fanout ratio (1:1, 1:many, broadcast?)
├── Message size and frequency
├── Delivery guarantees (at-most-once vs. at-least-once vs. exactly-once)
├── Ordering requirements
├── Geographic distribution
└── Acceptable latency (10ms vs. 1s are different problems)

Core Architectural Approaches

Approach 1: Single-Server with Vertical Scaling

code
Clients → [Single WebSocket Server with Event Loop]
               ↓
          [In-Memory State]

Implementation:

javascript
// Node.js handles ~65K connections per process before OS limits bite
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

const rooms = new Map(); // in-memory, fast

server.on('connection', (ws, req) => {
  const userId = authenticate(req);
  ws.userId = userId;
  
  ws.on('message', (data) => {
    const { type, payload } = JSON.parse(data);
    handleMessage(ws, type, payload);
  });
  
  ws.on('close', () => cleanup(userId));
});

// 100K connections requires tuning OS limits
// /etc/sysctl.conf:
// net.core.somaxconn = 65535
// net.ipv4.tcp_max_syn_backlog = 65535
// fs.file-max = 200000

Honest assessment:

  • ✅ Simple, low latency, no network hops for message routing
  • ❌ Single point of failure, unacceptable for production
  • ❌ 100K on one process is theoretically possible but practically fragile
  • ❌ Memory becomes the bottleneck (~50KB per connection = 5GB for 100K)

Verdict: Only defensible for internal tooling or prototypes.


Approach 2: Horizontally Scaled Servers + Pub/Sub Backbone

This is the most common production pattern and usually the right starting point.

code
                    [Load Balancer]
                    (sticky sessions)
                   /       |        \
           [WS Server 1] [WS Server 2] [WS Server 3]
                   \       |        /
                    [Pub/Sub Layer]
                    (Redis / Kafka)
                          |
                    [Message Source]
                    (Services / DB)

The core problem this solves:

code
Without pub/sub:
User A (on Server 1) sends message to User B (on Server 2)
→ Server 1 has no connection to User B
→ Message is lost

With pub/sub:
Server 1 publishes to channel "user:B"
Server 2 subscribes to "user:B", holds the connection
→ Message delivered

Implementation with Redis:

javascript
// connection-registry.js
class ConnectionRegistry {
  constructor(redisClient) {
    this.redis = redisClient;
    this.localConnections = new Map();
    this.serverId = process.env.SERVER_ID;
  }
  
  async register(userId, ws) {
    this.localConnections.set(userId, ws);
    
    // Register which server holds this user's connection
    await this.redis.setex(
      `user:server:${userId}`,
      300, // TTL, critical for cleanup on crash
      this.serverId
    );
    
    // Subscribe to this user's channel
    this.subscriber.subscribe(`notify:${userId}`);
  }
  
  async send(userId, message) {
    const localWs = this.localConnections.get(userId);
    
    if (localWs?.readyState === WebSocket.OPEN) {
      // Fast path: user is local
      localWs.send(JSON.stringify(message));
      return;
    }
    
    // Slow path: publish, let the right server deliver
    await this.redis.publish(`notify:${userId}`, JSON.stringify(message));
  }
  
  async handleIncoming(channel, message) {
    const userId = channel.replace('notify:', '');
    const ws = this.localConnections.get(userId);
    
    if (ws?.readyState === WebSocket.OPEN) {
      ws.send(message);
    }
    // If not found: user disconnected between publish and delivery
    // This is the at-most-once gap, address with persistence if needed
  }
}

Redis pub/sub trade-offs:

code
Redis Pub/Sub:
├── PRO: Simple, battle-tested, low latency (<1ms)
├── PRO: Handles fan-out efficiently
├── CON: No message persistence, if subscriber is down, message lost
├── CON: Single Redis node is a bottleneck (but Redis Cluster helps)
└── CON: Every server subscribes to every channel it needs = subscription overhead

Redis Streams (alternative):
├── PRO: Persistent, replayable
├── PRO: Consumer groups for load balancing
├── CON: More complex to implement correctly
└── CON: Storage costs at scale

Approach 3: Kafka-Backed Architecture (High-Throughput Variant)

When Redis pub/sub becomes a bottleneck (roughly >500K messages/sec or when you need replay/audit).

code
[Services] → [Kafka Topics] → [Consumer Group: WS Servers]
                                       ↓
                              [WebSocket Connections]

Topic Design:
notifications
├── Partition 0: userIds hashed to 0
├── Partition 1: userIds hashed to 1  
└── Partition N: ...

Each WS server owns partitions → predictable routing
python
# kafka_ws_bridge.py
from confluent_kafka import Consumer
import asyncio

class KafkaWebSocketBridge:
    def __init__(self, partitions_owned: list[int]):
        self.consumer = Consumer({
            'bootstrap.servers': 'kafka:9092',
            'group.id': 'websocket-servers',
            'auto.offset.reset': 'latest',
            # Critical: don't lose messages during rebalance
            'enable.auto.commit': False,
        })
        self.connections: dict[str, WebSocket] = {}
        
    async def process_messages(self):
        while True:
            msg = self.consumer.poll(timeout=0.01)
            if msg is None:
                await asyncio.sleep(0)  # yield to event loop
                continue
                
            notification = json.loads(msg.value())
            user_id = notification['user_id']
            
            if ws := self.connections.get(user_id):
                try:
                    await ws.send(json.dumps(notification))
                    self.consumer.commit(msg)
                except WebSocketDisconnected:
                    # Message lost, acceptable for notifications
                    # Unacceptable for financial events
                    del self.connections[user_id]

Kafka vs. Redis trade-off matrix:

DimensionRedis Pub/SubKafka
Latency~1ms~5-15ms
Throughput~100K msg/sec/nodeMillions/sec
PersistenceNo (Streams: yes)Yes
ComplexityLowHigh
ReplayNoYes
OrderingPer-channelPer-partition
Right scaleUp to ~10M users10M+ users

Approach 4: Purpose-Built Infrastructure

For cases where you shouldn't build this yourself:

code
Options:
├── Ably / Pusher
│   └── Right for: teams where WebSocket infra isn't a core competency
│
├── AWS API Gateway WebSocket API
│   ├── Pay-per-message pricing gets expensive fast
│   └── 100K connections = ~$300/month connection fees alone
│
└── Socket.IO (library, not service)
    ├── Handles reconnection, fallback transports
    └── But: higher per-connection overhead, proprietary protocol overhead

Critical Problems Often Underestimated

1. Connection State on Crash

javascript
// Naive approach, will cause ghost connections
process.on('SIGTERM', () => process.exit()); // WRONG

// Correct graceful shutdown
process.on('SIGTERM', async () => {
  // 1. Stop accepting new connections
  server.close();
  
  // 2. Notify clients to reconnect elsewhere
  for (const [userId, ws] of connections) {
    ws.send(JSON.stringify({ type: 'reconnect', backoff: 1000 }));
    ws.close(1001, 'Server shutting down');
  }
  
  // 3. Clean up registry
  await registry.deregisterAll();
  
  // 4. Wait for in-flight messages
  await drain();
  process.exit(0);
});

2. Client Reconnection (Most Implementations Get This Wrong)

javascript
// client.js, production reconnection logic
class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.backoff = new ExponentialBackoff({
      initial: 1000,
      max: 30000,
      jitter: true // CRITICAL: prevents thundering herd
    });
  }
  
  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onclose = (event) => {
      if (event.code === 1001) {
        // Server-initiated graceful close, reconnect immediately
        this.connect();
        return;
      }
      
      // Back off to avoid overwhelming recovering servers
      const delay = this.backoff.next();
      console.log(`Reconnecting in ${delay}ms`);
      setTimeout(() => this.connect(), delay);
    };
    
    this.ws.onopen = () => {
      this.backoff.reset();
      this.resubscribe(); // Re-register room memberships
    };
  }
}

// Without jitter: 100K clients reconnect simultaneously → DDoS yourself
// With jitter: reconnections spread over ~30s window

3. Memory Pressure Under Load

code
Per-connection overhead breakdown:
├── Kernel socket buffer: ~4-8KB (tunable)
├── TLS session state: ~10-20KB
├── Application state: ~20-50KB (depends on your code)
└── Total: ~50-80KB × 100K = 5-8GB

Implications:
- Dedicated WebSocket servers (don't colocate with DB)
- Monitor heap carefully in Node.js (GC pauses affect all connections)
- Consider connection limits per pod + autoscaling

Recommended Architecture Decision Tree

code
Start here:
│
├── Do you need message persistence/replay?
│   ├── YES → Kafka + persistent store
│   └── NO → Continue
│
├── Expected messages/second?
│   ├── <50K/sec → Redis Pub/Sub is fine
│   └── >50K/sec → Consider Kafka or Redis Cluster
│
├── Team size and operational maturity?
│   ├── Small team → Managed service (Ably/Pusher) or Redis
│   └── Platform team → Build with Kafka
│
└── Delivery guarantee requirements?
    ├── At-most-once (notifications OK if dropped)
    │   → Redis Pub/Sub, simple
    ├── At-least-once (user must see it eventually)
    │   → Persist + ack + client-side dedup
    └── Exactly-once (financial, critical)
        → Kafka + idempotency keys + transactional consumers

My Concrete Recommendation for 100K Connections

Start here:

code
[Load Balancer with sticky sessions]
        ↓
[4-8 WebSocket Servers, ~15-25K connections each]
        ↓
[Redis Cluster for pub/sub + connection registry]
        ↓
[PostgreSQL or DynamoDB for persistent notifications]
        ↓
[Message queue (SQS/RabbitMQ) for notification ingestion]

Why:

  1. Redis handles 100K connections comfortably
  2. Horizontal scaling is straightforward
  3. You can swap Redis for Kafka later if needed, the interface boundary is clean
  4. Lower operational complexity than Kafka for this scale
  5. Persistent store handles the "missed notifications while offline" problem separately from delivery

What to not do:

  • Avoid Socket.IO unless you specifically need its fallback transports, the overhead isn't worth it at this scale
  • Don't use Redis pub/sub without a persistence layer if any message matters
  • Don't skip client-side exponential backoff with jitter, it's a production incident waiting to happen

The real engineering work at 100K connections isn't the happy path, it's the crash recovery, reconnection storms, and partial failure handling.

Answer 3

To design a system for 100,000 concurrent whispers in a digital stadium, we must think less like civil engineers and more like mycologists or air traffic controllers. Handling 100K WebSockets isn't just about raw power; it’s about managing the "state of presence" and the "cost of silence."

Here are three architectural archetypes, viewed through the lens of different organizational philosophies.


1. The "Mycelial Network" (The Actor Model approach)

Core Tech: Elixir/Phoenix (The BEAM VM), Akka (JVM), or Proto.actor.

In a forest, mushrooms are just the fruit of a massive, underground fungal network (mycelium) that transports nutrients and signals across miles. In this architecture, every one of your 100K connections is treated as a "living entity" (an Actor).

  • How it works: Each WebSocket is a lightweight process (not an OS thread). These processes have their own tiny memory space and communicate via message passing.
  • The Beauty: The BEAM VM (built for Erlang) was designed for the phone system. It handles "soft real-time" flawlessly. If one connection crashes, it doesn’t take down its neighbors.
  • Trade-offs:
    • Pros: Incredible fault tolerance. You can reach 100K connections on a surprisingly small hardware footprint.
    • Cons: The "Niche Knowledge" tax. Finding Elixir or Scala/Akka experts is harder than finding Node.js developers.

2. The "Centralized Nervous System" (The Redis Pub/Sub approach)

Core Tech: Node.js/Go, Redis (as the Message Bus), Nginx/HAProxy.

This is the classic "Hub and Spoke" model. Imagine a giant central brain (Redis) sending electrical impulses out to various limbs (Web servers) which then relay them to the fingertips (users).

  • How it works: Your 100K users are spread across 5–10 microservices. When a notification is sent, it’s published to a Redis channel. Every server "subscribes" to the channels relevant to its currently connected users.
  • The Beauty: It scales horizontally with ease. If you hit 200K users, you just add more "limbs" (servers).
  • Trade-offs:
    • The "Double Delivery" Ghost: At high volumes, ensuring a message is delivered exactly once becomes a complex dance of acknowledgments.
    • The Redis Bottleneck: Redis is fast, but it’s single-threaded for many operations. At 100K+ messages per second, the "Nervous System" can suffer from cardiac arrest.

3. The "Atmospheric Edge" (The Serverless/Edge approach)

Core Tech: Cloudflare Durable Objects, AWS AppSync, or Fly.io.

Instead of bringing all 100K users to your data center in Virginia, you meet them at the "shore" of the internet. This is like a weather system, the clouds are everywhere, and the rain (data) falls locally.

  • How it works: You use a globally distributed edge network. The WebSocket terminates at the nearest Point of Presence (PoP). State is managed via "Durable Objects" or a distributed ledger that syncs globally.
  • The Beauty: Lowest possible latency. A user in Tokyo doesn't have to wait for a signal to travel to London and back.
  • Trade-offs:
    • Pros: Zero infrastructure management. The provider handles the scaling of the 100K sockets.
    • Cons: "Vendor Lock-in" and high cost. You pay a premium for the convenience of not managing the "pipes" yourself.

The Explorer’s Comparison Matrix

FeatureThe Mycelium (Actors)The Nervous System (Pub/Sub)The Atmosphere (Edge)
Philosophical RootIsolation & ResilienceCentralized CoordinationProximity & Speed
Scaling DifficultyLow (Internal to VM)Medium (Requires Load Balancers)Zero (Automated)
Resource EfficiencyHighest (Least RAM per socket)ModerateVariable (Price vs. Performance)
The "A-Ha!" FactorIf one process dies, nothing leaks.Familiar tools, massive community.No "Servers" to patch or wake up for.

The "Invisible" Challenges (The Breadth of Knowledge)

To make any of these work, you must solve the Three Specters of Real-time:

  1. The Thundering Herd: If your server restarts, 100K clients will try to reconnect simultaneously. This is a DDOS attack of your own making.
    • Solution: Jitter. Force clients to wait a random interval (0–60s) before reconnecting.
  2. The Zombie Connection: A phone goes into a tunnel. The server thinks the user is still there; the user thinks they are connected.
    • Solution: Heartbeats. Send a "ping" every 30 seconds. If no "pong" returns, reap the connection like a dead leaf.
  3. The File Descriptor Ceiling: Linux defaults to a small number of open files (often 1024). 100K connections require 100K file descriptors.
    • Solution: You must tune the OS kernel (ulimit -n 100000) or your system will "choke" long before the CPU gets warm.

A Novel Approach: The "Hybrid Butterfly"

Why use WebSockets for everything? WebSockets are expensive because they are "stateful", they hold a door open.

Consider SSE (Server-Sent Events) for the 100K notifications (unidirectional, lighter on the server) and only upgrade to WebSockets for the 5% of users who are actively typing or interacting. This is "Economic Engineering", using the cheapest tool for the widest part of the funnel.

Answer 4

Assumptions First: Scale Isn't Just Connections

100K concurrent WebSockets sounds impressive, but it's meaningless without RPS (notifications/sec), fan-out ratio (users per room/topic), or retention needs. If it's mostly idle connections with bursts, you're golden. If it's Twitter-scale firehose? Rethink. WebSockets aren't magic, SSE or push via service workers often beat them for one-way notifications (lower overhead, HTTP/2 multiplexing). But you asked for WebSockets, so let's dissect architectures. I'll compare four battle-tested ones, ranked by increasing complexity/maturity for your scale.

1. Multi-Process Horizontal Scaling (e.g., Node.js + Socket.io + Redis Adapter)

  • How it works: Stateless app servers behind a TCP load balancer (no sticky sessions needed). Redis Pub/Sub broadcasts messages across instances. Clients connect to any server; server proxies to Redis.
  • Pros: Simple, cheap (Commodity VMs/EC2). Handles 100K easily on 10-20 beefy instances (e.g., c5.4xlarge).
  • Cons: Redis single-thread bottleneck at ~100K ops/sec; sharding adds pain. Event loop blocking kills perf.
  • When: Low RPS (<10K/sec), cost-sensitive startups.

2. Broker-Heavy Pub/Sub (e.g., NATS/Kafka + App Servers)

  • How it works: Dedicated message brokers (NATS for speed, Kafka for durability). App servers handle auth/handshakes, fan-out via broker topics. Use gRPC or raw TCP for inter-service.
  • Pros: Decouples concerns, scale brokers independently. Kafka persists queues for replays; NATS hits <1ms latency.
  • Cons: Broker ops overhead (Kafka needs ZooKeeper/ KRaft). High fan-out explodes broker load.
  • When: High durability/replays needed (e.g., chat with offline delivery).

3. Actor/Event-Sourced Microservices (e.g., Akka/Elixir Phoenix + EventStore)

  • How it works: Each user/room as an actor/shard. Events stored in append-only log (EventStoreDB), projected to read models. Phoenix Channels shine here (Erlang VM handles millions).
  • Pros: Fault-tolerant (supervision trees), linearizable consistency. Elixir crushes 1M+ conns/node.
  • Cons: Steep learning curve, state explosion at scale (needs sharding). Overkill for fire-and-forget.
  • When: Complex state (e.g., multiplayer games, collaborative editing).

4. Managed PaaS/Serverless (e.g., Ably/Pusher, AWS API Gateway + Lambda, Supabase Realtime)

  • How it works: Offload everything, global edge network routes WebSockets, auto-scales pub/sub. AWS: API Gateway proxies to Lambda/DynamoDB streams.
  • Pros: Zero ops, 99.999% SLA, global low-latency (CDN-backed). Ably handles 1B msgs/day out-of-box.
  • Cons: Vendor lock-in, pricey at scale ($0.01-0.10/conn-hour + msg fees). Lambda cold starts suck for <100ms latency.
  • When: Speed to market > control. Most "scale nightmares" vanish here.

Trade-Offs Head-to-Head

ApproachScalability (100K conns)Latency (p99)Cost (Monthly est., 100K idle)Complexity/OpsReliabilityContrarian Take
Multi-ProcessGood (20 servers)10-50ms$500-2K (EC2+Redis)LowMedium (Redis SPoF)Underrated, 90% of "scale fails" are config errors, not arch. Skip if no Redis wizard.
Broker-HeavyExcellent (broker farms)5-20ms$1K-5K (EKS+Kafka)Medium-HighHighKafka cult is hype; NATS/JetStream does 99% cheaper/faster. Brokers bloat simple notifs.
Actor MicrosvcsGod-tier (5-10 nodes)1-10ms$2K-10K (K8s + DB)HighExcellentFunctional purists love it, but it's academic wankery for broadcasts. Phoenix OTP if you dare.
Managed PaaSInfinite (auto)10-100ms$5K-20K+ (Ably tier)ZeroExcellentLazy? Yes. Smart? Absolutely, reinventing WebSocket infra is 2024's cargo cult.

Maverick Recommendation: Don't Reinvent, Hybrid It

Start with Multi-Process + Redis for MVP (Socket.io adapter is plug-n-play). Benchmark with Artillery.io. Hit walls? Swap Redis for NATS. Need zero-ops? Migrate to Ably (free tier tests 100K). Avoid Kafka unless you're Netflix, overhead kills. Monitor conns with Prometheus, not vibes.

Provocative truth: 80% of "100K scale" claims are bullshit (zombie conns). Kill idle ones after 30min. And FFS, add heartbeats, your perf killer's probably pings, not arch. Got RPS numbers? I'll refine.

Synthesis

Combined answer

Real-Time Notification System: 100K Concurrent WebSocket Connections

Start With the Right Questions

Before choosing an architecture, three questions fundamentally change your answer:

1. What's your message throughput and fan-out pattern?
100K connections with 100 notifications/sec is a solved problem. 100K connections with 500K notifications/sec broadcasting to large groups is a different beast. The connection count is often not your bottleneck.

2. What are your delivery guarantees?

  • At-most-once (dropped notifications acceptable) → Redis Pub/Sub, simple
  • At-least-once (user must eventually see it) → persistence layer required
  • Exactly-once (financial, critical events) → Kafka + idempotency keys + transactional consumers

3. Is WebSocket actually the right transport?
For one-way notifications, Server-Sent Events (SSE) impose less overhead and work over HTTP/2 multiplexing. Reserve WebSockets for bidirectional interaction, and consider a hybrid: SSE for the 95% of users passively receiving, WebSockets only for the 5% actively interacting.


The Four Architectural Approaches

Approach 1: Horizontally Scaled WebSocket Servers + Message Broker

This is your baseline recommendation for 100K connections. Deploy a fleet of stateless WebSocket gateway servers behind a load balancer, coordinated via a shared pub/sub layer.

code
[Clients]
    ↓
[Load Balancer, sticky sessions optional]
    ↓              ↓              ↓
[WS Server 1]  [WS Server 2]  [WS Server 3]
    \               |              /
         [Pub/Sub Broker]
               ↓
    [Backend Services / Message Sources]
         ↓
    [Persistence Store, for missed messages]

Why sticky sessions are a preference, not a requirement: With pub/sub routing, any server can receive a message and publish it; the server holding the target connection delivers it. Sticky sessions reduce broker traffic by increasing local-delivery hits, but they're not architecturally necessary.

The message flow:

  1. A backend service emits a notification
  2. It publishes to a channel (e.g., notify:user:42)
  3. All WebSocket servers subscribing to that channel receive it
  4. The server holding user 42's connection delivers it; others discard

Broker choice matters:

Redis Pub/SubRedis StreamsNATS/JetStreamKafka
Latency~1ms~2ms<1ms5–15ms
PersistenceNoYesYes (JetStream)Yes
Throughput ceiling~100K msg/secHighVery highMillions/sec
Operational complexityLowLow-MediumMediumHigh
Right scaleUp to ~10M usersSame + auditSame + reliability10M+ or replay needs

Concrete recommendation: Start with Redis Pub/Sub. It handles 100K connections comfortably. If you need persistence (missed-message recovery), add a PostgreSQL/DynamoDB store as a separate concern, don't conflate delivery with durability. If throughput exceeds ~100K msg/sec, migrate the broker to NATS JetStream before considering Kafka. Kafka's operational overhead is frequently not worth it unless you specifically need log replay or have a dedicated platform team.


Approach 2: Actor-Based / BEAM Architecture

If your use case involves complex per-connection state, collaborative editing, multiplayer games, rooms with rich logic, the actor model deserves serious consideration.

Elixir/Phoenix Channels run on the BEAM VM, where each WebSocket connection is a lightweight Erlang process (~2KB overhead). A single node can comfortably hold 1M+ connections, and supervision trees provide automatic crash recovery. WhatsApp famously ran 2 million concurrent connections on a single Erlang node.

Trade-offs:

  • ✅ Extraordinary resource efficiency per connection
  • ✅ Fault isolation, one crashing process doesn't affect neighbors
  • ✅ Natural state management for complex per-user logic
  • ❌ Requires Elixir/Erlang expertise, hiring is harder
  • ❌ Still needs a pub/sub layer for cross-cluster global events
  • ❌ Overkill for simple broadcast notifications

When to choose it: You have teams with BEAM expertise, complex stateful interactions per connection, or you're building something like a game server or collaborative editor. For a notification system that's primarily push-and-forget, this is probably over-engineered.


Approach 3: Managed / Cloud-Native Services

Offload the infrastructure entirely to providers like Ably, Pusher, Azure Web PubSub, or AWS API Gateway WebSocket API.

Genuine strengths:

  • Zero operational overhead for connection management and scaling
  • Global edge networks provide low latency internationally
  • Automatic handling of reconnection, fan-out, and failover

Genuine weaknesses:

  • Cost scales badly. AWS API Gateway WebSocket charges ~$3.50/million connection-minutes plus message fees. At 100K connections for a month, connection costs alone exceed $5K–$15K before a single message is sent.
  • Vendor lock-in is real. Custom routing logic, protocol extensions, and unusual delivery semantics become difficult or impossible.
  • Lambda cold starts behind API Gateway introduce 100–500ms latency spikes.

When to choose it: Time-to-market is your primary constraint, you have unpredictable spiky load that would require significant autoscaling engineering, or your team has no infrastructure expertise. Ably's free tier is also excellent for benchmarking and load testing your expected traffic patterns before committing to a self-hosted design.


Approach 4: Monolithic Single-Server

A single WebSocket server, Node.js with ws, Go with gorilla/websocket, can technically handle 100K connections on a tuned machine. This is the right choice for prototyping only.

In production: one process failure drops all 100K connections simultaneously. This is architecturally unacceptable for any real service.


The Problems Everyone Underestimates

These failure modes cause more production incidents than architectural choice.

1. The Thundering Herd on Reconnect

If your server restarts, 100K clients will attempt to reconnect within seconds. This self-inflicted DDoS will take down whatever they're reconnecting to.

Solution, always implement exponential backoff with jitter:

javascript
class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.baseDelay = 1000;
    this.maxDelay = 30000;
    this.attempt = 0;
  }

  nextDelay() {
    // Exponential backoff
    const exp = Math.min(this.baseDelay * 2 ** this.attempt, this.maxDelay);
    // Full jitter, spreads 100K reconnects over the backoff window
    return Math.random() * exp;
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onopen = () => { this.attempt = 0; this.resubscribe(); };
    this.ws.onclose = (event) => {
      if (event.code === 1001) { this.connect(); return; } // Server-initiated graceful close
      setTimeout(() => { this.attempt++; this.connect(); }, this.nextDelay());
    };
  }
}

Without jitter, even a staggered restart causes a spike. With full jitter, 100K reconnects spread across the backoff window.

2. Zombie Connections and Heartbeats

A mobile client enters a tunnel. The OS-level TCP connection is gone, but your server still holds the socket open, accumulating phantom connections that consume memory and file descriptors.

Solution: Send a ping every 30 seconds. If no pong returns within 10 seconds, close the connection and clean up the registry entry. Most WebSocket libraries support this natively, enable it.

3. OS File Descriptor Limits

Linux defaults to 1,024 open file descriptors per process. 100K connections require 100K file descriptors. Your system will refuse connections long before the CPU is warm.

bash
# /etc/sysctl.conf
fs.file-max = 500000
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# Per-process limit
ulimit -n 200000

4. Memory Pressure Is Real

Per-connection overhead breakdown:

  • Kernel socket buffer: 4–8KB
  • TLS session state: 10–20KB
  • Application state: 20–50KB depending on your code

At 100K connections: 5–8GB RAM dedicated to connection state alone. WebSocket servers should not colocate with databases or other memory-intensive services. Dedicated pods/VMs with monitored heap usage are not optional.

5. Graceful Shutdown

javascript
process.on('SIGTERM', async () => {
  server.close(); // Stop accepting new connections
  
  for (const [userId, ws] of connections) {
    ws.send(JSON.stringify({ type: 'reconnect', backoffMs: 1000 }));
    ws.close(1001, 'Server shutting down');
  }
  
  await registry.deregisterAll(); // Clean up Redis entries
  await drain();                  // Wait for in-flight messages
  process.exit(0);
});

Without this, clients don't know to reconnect, and your registry accumulates stale entries that cause missed message deliveries.


Recommended Architecture for 100K Connections

code
[Load Balancer]
      ↓
[4–8 WebSocket Servers, ~15–25K connections each]
      ↓
[Redis Cluster, Pub/Sub + Connection Registry with TTL]
      ↓                              ↓
[Message Queue, SQS/RabbitMQ]   [PostgreSQL/DynamoDB, persistent notifications]
      ↓
[Backend Services, notification producers]

Stack rationale:

  • Redis for pub/sub and registry: connection registry entries use TTLs (e.g., 5-minute expiry, refreshed by heartbeats) so server crashes don't leave ghost registrations
  • Separate persistence store: Redis pub/sub doesn't persist, if a user is offline, the message is lost. Solve "missed while offline" in the database layer, not the delivery layer; these are separate concerns
  • Message queue for ingestion: decouples notification producers from delivery speed, provides backpressure

Scaling path:

  • 100K connections: 4–6 nodes, single Redis instance
  • 500K connections: 10–15 nodes, Redis Cluster
  • 1M+ connections: Consider NATS JetStream or BEAM-based architecture, or evaluate managed services against operational cost

Decision Framework

code
Are delivery guarantees critical (financial, legal)?
├── YES → Kafka + idempotency keys + transactional consumers
└── NO → Continue

Expected messages/second?
├── <50K/sec → Redis Pub/Sub
├── 50K–500K/sec → Redis Cluster or NATS
└── >500K/sec → Kafka or NATS JetStream

Team infrastructure expertise?
├── Low → Managed service (Ably, Azure Web PubSub)
└── Medium/High → Self-hosted with Redis

Per-connection state complexity?
├── Simple (push notifications) → Horizontally scaled servers + Redis
└── Complex (games, collaborative) → Elixir/Phoenix or Akka

Cost sensitivity?
├── High → Self-hosted Multi-process + Redis (~$500–2K/month)
└── Low → Managed services (offload ops, pay premium)

What to Benchmark Before Committing

Use Artillery.io or k6 to simulate your actual load pattern, not just connection count. The benchmark that matters is:

  1. Sustained connections at your target count
  2. Burst message delivery at your peak notification rate
  3. Reconnection storm, simulate a full server restart with all clients reconnecting
  4. Memory growth over time, zombie connections accumulate slowly and aren't visible in short tests

The architecture that looks correct on paper frequently breaks on reconnection storms and zombie connection accumulation. Build and test those failure scenarios before you're in production.

How Synero works

1Ask your question

Your question goes to GPT, Claude, Gemini and Grok. Each answers on its own.

2Get one answer

Synero writes one combined answer from the best of all four.

3See where they split

Agreement is a good sign. Disagreement tells you what to check.

Benefits

Why developers choose Synero

Architecture Review

Get your system designs reviewed from four analytical angles (scalability, maintainability, innovation, and risk) before writing a single line of code.

Code Debugging

Paste error logs or problematic code and get four independent diagnostic analyses. The synthesized answer identifies the most likely root cause and recommended fix.

Technology Selection

Evaluating frameworks, databases, or cloud services? Several models compare options independently (performance, DX, ecosystem, and contrarian picks), and the agreement panel shows where they split, helping you avoid hype-driven decisions.

Documentation & Communication

Generate clear technical documentation, RFC drafts, and architecture decision records that explain complex decisions from multiple stakeholder perspectives.

See it in 30 seconds

GPTClaudeGeminiGrok
Try it free

FAQ

Common questions from developers

Which AI coding models does Synero use?

Synero supports frontier models from OpenAI (GPT-6.1 Sol, GPT-6 Astra, GPT-5.6), Anthropic (Claude Opus, Claude Sonnet), Google (Gemini Pro, Gemini Flash), and xAI (Grok). You choose 1 to 4 of 20 models with one click, so you can ask the best coding models across providers side by side. The multi-model approach means you're not locked into any single model's strengths or weaknesses.

Can Synero review my code?

Yes. Paste your code and ask for a review: you'll get four independent analyses covering different aspects like correctness, performance, security, readability, and maintainability. The synthesized answer highlights the most important issues and provides prioritized recommendations. It's like having four senior engineers review your PR.

How does multi-model analysis help compared to GitHub Copilot?

Copilot excels at inline code completion: it's a productivity tool for writing code faster. Synero excels at higher-level thinking: architecture decisions, debugging complex issues, technology evaluation, and system design. They're complementary: use Copilot for writing code and Synero for thinking about code.

Can Synero handle system design and architecture questions?

This is one of Synero's strongest use cases. System design questions inherently benefit from multiple perspectives: scalability vs. simplicity, performance vs. cost, proven patterns vs. innovative approaches. Different models tend to land on different sides of these trade-offs, and the agreement panel helps you navigate them.

Ready to build better software?

Get architectural insights from four AI perspectives before making your next technical decision.

Start free

3 free questions, no card. Then $10/month with $10 of usage included, or pay as you go with credit packs from $5, no subscription. Private by default, never used for training. Pricing · How it works · More use cases