Skip to main content

Benchmarks

This page reports the scalability benchmark results of Realtime and the PG service on a single-instance deployment. It covers five areas — Broadcast (public and private channels), payload size, connection and authorization rate, Presence, and Postgres Changes — and gives the recommended production capacity and configuration parameters.

The subject under test is a single-instance Realtime Server. Re-run the benchmarks against your own workload before relying on these numbers.


Methodology​

Test environment​

ItemConfiguration
Realtime Server2 vCPU / 4 GB × 1
PostgreSQL1 vCPU / 2 GB

Tenant quotas (during the test)​

QuotaValueNotes
max_concurrent_users30,000Upper bound on active WebSocket connections per tenant. New connections are rejected above it (TooManyConnections).
max_joins_per_second3,000Upper bound on channel joins per second per tenant. Join requests are rejected above it (TooManyJoins).
max_events_per_second30,000Judged on a 60-second average
max_presence_events_per_second10,000Counted by deliveries
max_bytes_per_second10 MB/sInbound traffic only; no effect on high fan-out
db_pool (authorization pool)10Determines the authorization throughput ceiling
subs_pool_size (subscription pool)10Determines the CDC circuit-breaker threshold

Test method​

ItemDescription
ProtocolRaw WebSocket + Phoenix protocol, one join per connection
Ramp-upStepped ramp at 50/s, then sustained sending at steady state
MetricsRate metrics use a 60-second sliding average; latency is measured from send to first client receipt
Observed dataConnection success rate, delivery rate, deliveries per second, p50/p95/p99 latency, server CPU and memory

Metric definitions​

MetricDefinition
Messages sent per secondOne message published to a channel counts as 1, regardless of subscriber count
Deliveries per secondMessages pushed to clients = messages sent × subscriber count
Channel join rateOne count per client that connects and joins a channel
Delivery rateMessages actually received ÷ messages expected

1. Broadcast over WebSocket (public)​

Public channels skip authorization. All connections subscribe to the same topic, and a single sender broadcasts at a fixed rate. This measures the capacity of the pure fan-out path.

1.1 Connection count​

ConnectionsServer memoryDelivery rateResult
20,00082%100%Stable
24,00091.6%100%Near limit
25,000Inflection100%Unusable

Memory grows linearly with the connection count: memory ≈ 35% + 4 percentage points × (connections ÷ 1000). Single-instance memory is the first constraint on connections: 20,000 is the recommended ceiling, and memory is exhausted at 25,000.

1.2 Delivery volume​

Deliveries per secondServer CPULatencyResult
5,00018%LowStable
19,59152%LowStable
27,00070%NormalSafe line
33,98888%Starting to riseNear limit
40,000~95%SecondsDegraded
45,80094.8% plateaup50 21 s / p95 52 sCapacity ceiling, no loss

CPU correlates linearly with delivery volume and is independent of the connection count: CPU ≈ 6.5% + 2.35% × (deliveries per second ÷ 1000). At 45,800/s the system still delivers everything without loss, but latency is unacceptable — an "experience degrades before functionality does" failure mode.


2. Broadcast over Private Channels​

Every publish on a private channel runs an RLS authorization check and writes to the message table, and fan-out within a channel is serial. All tests ran with 4,000 connections; every connection joined successfully and none dropped.

2.1 Send and delivery combinations​

ScenarioTopologySent/sDelivered/sFirst-report p50Delivery rateResult
High-frequency steady500 rooms × 8 users, 5 msg/s2.5k17.2k77 ms100%Stable
Low-frequency steady2,000 rooms × 2 users, 2.5 msg/s5.0k4.9k64 ms100%Stable
High-frequency overload500 rooms × 8 users, 10 msg/s4.6k24.7k1,586 ms78%Latency degraded
Mid-frequency overload2,000 rooms × 2 users, 5 msg/s10.0k8.1k93 ms (p95 6.4 s)82%Latency degraded

"Steady" and "overload" contrast the capacity boundary under the same load model. All runs used 4,000 connections; all joined successfully and none dropped.

Private broadcast capacity is constrained from two directions: messages sent per second reflects authorization and dispatch cost, deliveries per second reflects fan-out cost. Sending 2.5k/s with 17.2k/s delivered and sending 5.0k/s with 4.9k/s delivered both hold tens-of-milliseconds latency at 100% delivery. At 24.7k/s delivered (4.6k/s sent), first-report latency rises to 1.59 s; at 10.0k/s sent, p95 latency reaches 6.4 s and delivery rate drops to 82%. For a single instance, keep both of these true: sent per second ≤ 6,000, delivered per second ≤ 17,000.

2.2 Connection count​

ConnectionsServer memoryDelivery rateResult
8,000—100%Stable
16,00072.8%100%Stable
20,00088.3%100%Near limit
24,000100%2.13% lostUnusable

Private broadcast has higher resident memory than public broadcast (each channel retains authorization and subscription state), so its connection ceiling is lower.


3. Payload Size​

Public broadcast scenario, 8,000 connections, fixed send rate, only the message size varies.

PayloadDelivery rateFan-out span p50Egress bandwidthResult
64 B100%387 ms—Stable
1 KB100%387 ms—Stable
10 KB100%——Stable
100 KB3 of 5 sent5,222 ms114 MiB/s ≈ 1 GbpsUnusable

Single-instance egress bandwidth is about 150 MB/s (~1.2 Gbps), with the inflection point between 10 KB and 100 KB. The bandwidth bottleneck shows up as the sender being unable to send, not as packet loss on the receiver.


4. Connection and Authorization Rate​

Joining a private channel runs one RLS authorization query, which occupies the authorization connection pool, so the connection rate has a hard ceiling.

Connection rateJoin success rateResult
50/s100%Stable
100/s15,998 / 16,000Stable
120/s100%Near ceiling
130/s0 deliveredTripped
200/s58.3% rejected (9,330 failed)Tripped

The mechanism is authorization pool ÷ per-authorization time ≈ 10 ÷ 80 ms ≈ 125/s. Above the limit the server returns IncreaseConnectionPool and recovers after a 30-second window. The limit rejects everything rather than degrading, so clients must implement reconnection backoff with jitter to avoid a second trip caused by mass reconnection.


5. Presence​

Each Presence state change is broadcast to every member of the channel, so cost grows with the square of the member count.

Members per topicScenarioResult
816,000 connections, 10% updates12,670/s, p50 85 ms, zero errors
825% updates23,497/s, p50 796 ms, zero errors
870% updates~78,000/s, p50 3,444 ms, memory 97.5%
10,000Single hot topicCPU 99.6% / memory 100%, 174 KB snapshot per user, 4,228 connections dropped

Quota requirement is calculated as:

Required quota = members² × update frequency
Per-topic update frequency ceiling = quota ÷ members

At a quota of 1,000/s:

MembersAllowed update frequency
8125/s
2005/s
1,0001/s
10,0000.1/s (unusable)

Conclusion: keep members per topic under 200. Do not use Presence for lobby-style scenarios.


6. Postgres Changes​

Every change is delivered one subscription at a time, and Postgres Changes is processed by a single-threaded poller to preserve ordering.

SubscriptionsSubscription reconciliationDelivery (4 rounds)Polling peakResult
16,000100%4 × 16,000, 0 missing390.8 msStable
17,000100%4 × 17,000, 0 missing612.2 msStable
18,00017,743 (257 short)Degraded840.7 msTripped
18,000 (retry)17,774 (226 short)Degraded430.8 msTripped

End-to-end latency at 16,000: p50 1,317 ms, p95 ≤ 1,694 ms, p99 ≤ 1,710 ms. Sustained change rate measured 3,000–5,000/s. Under mixed load with broadcast and Presence, CDC latency degrades about 5.8× (from 2.6 s to 15 s).

The trip is caused by poller failures clearing subscriptions; when clients re-register, they hit the subscription pool ceiling. Enlarging the subscription pool only postpones the trip — it does not fix the root cause.


7. Capacity Summary (recommended)​

No.DimensionRecommended (production)
1Public broadcast — concurrent connections≤ 20,000
2Private broadcast — concurrent connections≤ 18,000
3Public broadcast — deliveries per second≤ 27,000/s (CPU 70%)
4Private broadcast — messages sent per second≤ 6,000/s
5Private broadcast — deliveries per second≤ 17,000/s
6Write-only (no receivers)Unlimited (≥ 28,600/s did not hit a bottleneck)
7Connection / channel join rate≤ 60/s
8Message size≤ 4 KB
9CDC subscriptions per tenant≤ 14,000–15,000
10CDC — changes per second≤ 3,000/s
11Presence members per topic≤ 200
12Presence — deliveries per second≤ 23,000/s

These are single-instance safety lines. Beyond them, latency rises first; only sustained pressure causes delivery loss or disconnects. Data is never lost silently. When your workload exceeds the recommendations, add instances and shard by room or tenant — adding memory alone does not help, because the bottleneck is CPU.


8. Configuration Recommendations​

All parameters below are standard settings adjustable on the platform side; no code change or service restart is required.

Setting (config key)RecommendedRationaleBenefit
Database connection pool size (db_pool)10Private channel joins and REST publishes each need one RLS check; throughput ≈ "pool size ÷ 80 ms each"Join throughput from 125/s to ~375/s, red line from 60/s to 180/s
Postgres Changes connection pool size (subs_pool_size)20This pool is used only to create subscriptions; the CDC trip threshold equals the pool sizePostpones the CDC trip — a mitigation, not a fix
Max events per second (max_events_per_second)25,000Counted by deliveries; measured safe line is 27,000/s (CPU 70%)Aligns with actual capacity, avoids needless throttling
Max presence events per second (max_presence_events_per_second)10,000Each state change is broadcast to every client in the channel and also counts toward Max events per secondSupports 200-member topics with second-level updates