From fb30063d41079b597ad61f5c4ab698d3c73c7b53 Mon Sep 17 00:00:00 2001
From: egor <58992960+egor-romanov@users.noreply.github.com>
Date: Mon, 15 Jan 2024 14:24:47 +0400
Subject: [PATCH] docs: add form to estimate realtime throughput (#20260)
* docs: add form to estimate realtime throughput
* fix: compute select resetting rls and filters
* fix: typos
* docs: info on compute impact on realtime streaming
* docs: add 200k realtime test results
* docs: fix realtime single thread processing desc
Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
* docs: fix copy for realtime throughput estimation
Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
* fix: review comments and add collapsed table view
* fix: change concurrency to connected clients
* docs: add rt 100k test results
---------
Co-authored-by: Charis <26616127+charislam@users.noreply.github.com>
---
.../components/RealtimeLimitsEstimater.tsx | 203 +++++++++++++++
apps/docs/data/realtime/throughput.json | 232 ++++++++++++++++++
.../guides/realtime/postgres-changes.mdx | 41 +---
3 files changed, 440 insertions(+), 36 deletions(-)
create mode 100644 apps/docs/components/RealtimeLimitsEstimater.tsx
create mode 100644 apps/docs/data/realtime/throughput.json
diff --git a/apps/docs/components/RealtimeLimitsEstimater.tsx b/apps/docs/components/RealtimeLimitsEstimater.tsx
new file mode 100644
index 00000000000..d8e153c2d3b
--- /dev/null
+++ b/apps/docs/components/RealtimeLimitsEstimater.tsx
@@ -0,0 +1,203 @@
+import { useState } from 'react'
+import { Select, Collapsible, Button, IconChevronDown } from 'ui'
+import throughputTable from '~/data/realtime/throughput.json'
+
+export default function RealtimeLimitsEstimater({}) {
+ const findTableValue = ({ computeAddOn, filters, rls, concurrency }) => {
+ return throughputTable.find(
+ (l) =>
+ l.computeAddOn === computeAddOn &&
+ l.filters === filters &&
+ l.rls === rls &&
+ l.concurrency === concurrency
+ )
+ }
+
+ const [computeAddOn, setComputeAddOn] = useState('micro')
+ const [filters, setFilters] = useState(false)
+ const [rls, setRLS] = useState(false)
+ const [concurrency, setConcurrency] = useState(500)
+
+ const [limits, setLimits] = useState(findTableValue({ computeAddOn, filters, rls, concurrency }))
+
+ const [expandPreview, setExpandPreview] = useState(false)
+
+ const handleComputeAddOnSelection = (e) => {
+ const val = e.target.value
+ setComputeAddOn(val)
+ setConcurrency(500)
+ setLimits(findTableValue({ computeAddOn: val, filters, rls, concurrency: 500 }))
+ }
+
+ const handleFiltersSelection = (e) => {
+ const val = e.target.value.toLowerCase() === 'true'
+ setFilters(val)
+ setConcurrency(500)
+ setLimits(findTableValue({ computeAddOn, filters: val, rls, concurrency: 500 }))
+ }
+
+ const handleRLSSelection = (e) => {
+ const val = e.target.value.toLowerCase() === 'true'
+ setRLS(val)
+ setConcurrency(500)
+ setLimits(findTableValue({ computeAddOn, filters, rls: val, concurrency: 500 }))
+ }
+
+ const handleConcurrencySelection = (e) => {
+ const val = parseInt(e.target.value)
+ setConcurrency(val)
+ setLimits(findTableValue({ computeAddOn, filters, rls, concurrency: val }))
+ }
+
+ return (
+
+
Set your expected parameters
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {limits && (
+
+
Current maximum possible throughput
+
+
+
+
+ | Total DB changes /sec |
+ Max messages per client /sec |
+ Max total messages /sec |
+ Latency p95 |
+
+
+
+
+ | {limits.maxDBChanges} |
+ {limits.maxMessagesPerClient} |
+
+ {Intl.NumberFormat().format(limits.totalMessagesPerSecond)}
+ |
+ {limits.p95Latency}ms |
+
+
+
+
+ )}
+
+
+
+
+
View raw throughput table
+
+ }
+ className="px-1"
+ onClick={() => setExpandPreview(!expandPreview)}
+ />
+
+
+
+
+ {throughputTable
+ .map((l) => l.computeAddOn)
+ .filter((v, i, a) => a.indexOf(v) === i)
+ .map((computeAddOn) => (
+
+
+ {computeAddOn === 'micro'
+ ? 'Micro'
+ : computeAddOn === 'small'
+ ? 'Small to medium'
+ : 'Large to 16XL'}
+
+
+
+
+ | Filters |
+ RLS |
+ Connected clients |
+ Total DB changes /sec |
+ Max messages per client /sec |
+ Max total messages /sec |
+ Latency p95 |
+
+
+
+ {throughputTable
+ .filter((l) => l.computeAddOn === computeAddOn)
+ .map((l) => (
+
+ | {l.filters ? '✅' : '🚫'} |
+ {l.rls ? '✅' : '🚫'} |
+
+ {Intl.NumberFormat().format(l.concurrency)}
+ |
+ {l.maxDBChanges} |
+ {l.maxMessagesPerClient} |
+
+ {Intl.NumberFormat().format(l.totalMessagesPerSecond)}
+ |
+ {l.p95Latency}ms |
+
+ ))}
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/apps/docs/data/realtime/throughput.json b/apps/docs/data/realtime/throughput.json
new file mode 100644
index 00000000000..2afc1e6d862
--- /dev/null
+++ b/apps/docs/data/realtime/throughput.json
@@ -0,0 +1,232 @@
+[
+ {
+ "computeAddOn": "micro",
+ "filters": false,
+ "rls": false,
+ "concurrency": 500,
+ "maxDBChanges": 64,
+ "maxMessagesPerClient": 64,
+ "totalMessagesPerSecond": 32000,
+ "p95Latency": 238
+ },
+ {
+ "computeAddOn": "micro",
+ "filters": false,
+ "rls": false,
+ "concurrency": 5000,
+ "maxDBChanges": 10,
+ "maxMessagesPerClient": 10,
+ "totalMessagesPerSecond": 50000,
+ "p95Latency": 807
+ },
+ {
+ "computeAddOn": "micro",
+ "filters": false,
+ "rls": false,
+ "concurrency": 10000,
+ "maxDBChanges": 5,
+ "maxMessagesPerClient": 5,
+ "totalMessagesPerSecond": 50000,
+ "p95Latency": 1310
+ },
+ {
+ "computeAddOn": "micro",
+ "filters": false,
+ "rls": false,
+ "concurrency": 30000,
+ "maxDBChanges": 1,
+ "maxMessagesPerClient": 1,
+ "totalMessagesPerSecond": 30000,
+ "p95Latency": 941
+ },
+ {
+ "computeAddOn": "micro",
+ "filters": false,
+ "rls": true,
+ "concurrency": 500,
+ "maxDBChanges": 30,
+ "maxMessagesPerClient": 6,
+ "totalMessagesPerSecond": 3000,
+ "p95Latency": 228
+ },
+ {
+ "computeAddOn": "micro",
+ "filters": false,
+ "rls": true,
+ "concurrency": 1500,
+ "maxDBChanges": 10,
+ "maxMessagesPerClient": 2,
+ "totalMessagesPerSecond": 3000,
+ "p95Latency": 356
+ },
+ {
+ "computeAddOn": "micro",
+ "filters": false,
+ "rls": true,
+ "concurrency": 3000,
+ "maxDBChanges": 5,
+ "maxMessagesPerClient": 1,
+ "totalMessagesPerSecond": 3000,
+ "p95Latency": 616
+ },
+ {
+ "computeAddOn": "small",
+ "filters": false,
+ "rls": false,
+ "concurrency": 500,
+ "maxDBChanges": 64,
+ "maxMessagesPerClient": 64,
+ "totalMessagesPerSecond": 32000,
+ "p95Latency": 184
+ },
+ {
+ "computeAddOn": "small",
+ "filters": false,
+ "rls": false,
+ "concurrency": 5000,
+ "maxDBChanges": 10,
+ "maxMessagesPerClient": 10,
+ "totalMessagesPerSecond": 50000,
+ "p95Latency": 782
+ },
+ {
+ "computeAddOn": "small",
+ "filters": false,
+ "rls": false,
+ "concurrency": 10000,
+ "maxDBChanges": 5,
+ "maxMessagesPerClient": 5,
+ "totalMessagesPerSecond": 50000,
+ "p95Latency": 1349
+ },
+ {
+ "computeAddOn": "small",
+ "filters": false,
+ "rls": false,
+ "concurrency": 35000,
+ "maxDBChanges": 1,
+ "maxMessagesPerClient": 1,
+ "totalMessagesPerSecond": 35000,
+ "p95Latency": 1287
+ },
+ {
+ "computeAddOn": "small",
+ "filters": false,
+ "rls": true,
+ "concurrency": 500,
+ "maxDBChanges": 30,
+ "maxMessagesPerClient": 6,
+ "totalMessagesPerSecond": 3000,
+ "p95Latency": 282
+ },
+ {
+ "computeAddOn": "small",
+ "filters": false,
+ "rls": true,
+ "concurrency": 1500,
+ "maxDBChanges": 10,
+ "maxMessagesPerClient": 2,
+ "totalMessagesPerSecond": 3000,
+ "p95Latency": 387
+ },
+ {
+ "computeAddOn": "small",
+ "filters": false,
+ "rls": true,
+ "concurrency": 3000,
+ "maxDBChanges": 5,
+ "maxMessagesPerClient": 1,
+ "totalMessagesPerSecond": 3000,
+ "p95Latency": 920
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": false,
+ "concurrency": 500,
+ "maxDBChanges": 64,
+ "maxMessagesPerClient": 64,
+ "totalMessagesPerSecond": 32000,
+ "p95Latency": 184
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": false,
+ "concurrency": 5000,
+ "maxDBChanges": 10,
+ "maxMessagesPerClient": 10,
+ "totalMessagesPerSecond": 50000,
+ "p95Latency": 672
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": false,
+ "concurrency": 10000,
+ "maxDBChanges": 5,
+ "maxMessagesPerClient": 5,
+ "totalMessagesPerSecond": 50000,
+ "p95Latency": 1253
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": false,
+ "concurrency": 35000,
+ "maxDBChanges": 1,
+ "maxMessagesPerClient": 1,
+ "totalMessagesPerSecond": 35000,
+ "p95Latency": 1257
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": false,
+ "concurrency": 100000,
+ "maxDBChanges": "0.1 (6/min)",
+ "maxMessagesPerClient": "0.1 (6/min)",
+ "totalMessagesPerSecond": 40000,
+ "p95Latency": 4951
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": false,
+ "concurrency": 200000,
+ "maxDBChanges": "0.05 (3/min)",
+ "maxMessagesPerClient": "0.05 (3/min)",
+ "totalMessagesPerSecond": 40000,
+ "p95Latency": 4581
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": true,
+ "concurrency": 500,
+ "maxDBChanges": 40,
+ "maxMessagesPerClient": 8,
+ "totalMessagesPerSecond": 4000,
+ "p95Latency": 618
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": true,
+ "concurrency": 2000,
+ "maxDBChanges": 10,
+ "maxMessagesPerClient": 2,
+ "totalMessagesPerSecond": 4000,
+ "p95Latency": 606
+ },
+ {
+ "computeAddOn": "large",
+ "filters": false,
+ "rls": true,
+ "concurrency": 4000,
+ "maxDBChanges": 5,
+ "maxMessagesPerClient": 1,
+ "totalMessagesPerSecond": 4000,
+ "p95Latency": 918
+ }
+]
diff --git a/apps/docs/pages/guides/realtime/postgres-changes.mdx b/apps/docs/pages/guides/realtime/postgres-changes.mdx
index 3dedcf71c25..b4305c293c8 100644
--- a/apps/docs/pages/guides/realtime/postgres-changes.mdx
+++ b/apps/docs/pages/guides/realtime/postgres-changes.mdx
@@ -1,5 +1,6 @@
import Layout from '~/layouts/DefaultGuideLayout'
import StepHikeCompact from '~/components/StepHikeCompact'
+import RealtimeLimitsEstimater from '~/components/RealtimeLimitsEstimater'
export const meta = {
title: 'Postgres Changes',
@@ -1361,45 +1362,13 @@ Realtime systems usually require forethought because of their scaling dynamics.
There can be a database bottleneck which limits message throughput. If your database cannot authorize the changes rapidly enough, the changes will be delayed until you receive a timeout.
+Database changes are processed on a single thread to maintain the change order. That means compute upgrades don't have a large effect on the performance of Postgres change subscriptions. You can estimate the expected maximum throughput for your database below.
+
If you are using Postgres Changes at scale, you should consider using separate "public" table without RLS and filters. Alternatively, you can use Realtime server-side only and then re-stream the changes to your clients using a Realtime Broadcast.
-From our observations, we recommend the following limits depending on your database size:
+Enter your database settings to estimate the maximum throughput for your instance:
-### Micro
-
-| Filters | RLS Usage | Concurrent Clients | DB Changes per second (total) | Records per second per client | Messages per second (total) | Latency p95 (ms) |
-| ------- | --------- | ------------------ | ----------------------------- | ----------------------------- | --------------------------- | ---------------- |
-| 🚫 | 🚫 | 500 | 64 | 64 | 32,000 | 238 |
-| 🚫 | 🚫 | 5,000 | 10 | 10 | 50,000 | 807 |
-| 🚫 | 🚫 | 10,000 | 5 | 5 | 50,000 | 1310 |
-| 🚫 | 🚫 | 30,000 | 1 | 1 | 30,000 | 941 |
-| 🚫 | ✅ | 500 | 30 | 6 | 3,000 | 228 |
-| 🚫 | ✅ | 1,500 | 10 | 2 | 3,000 | 356 |
-| 🚫 | ✅ | 3,000 | 5 | 1 | 3,000 | 616 |
-
-### Small to medium
-
-| Filters | RLS Usage | Concurrent Clients | DB Changes per second (total) | Records per second per client | Messages per second (total) | Latency p95 (ms) |
-| ------- | --------- | ------------------ | ----------------------------- | ----------------------------- | --------------------------- | ---------------- |
-| 🚫 | 🚫 | 500 | 64 | 64 | 32,000 | 184 |
-| 🚫 | 🚫 | 5,000 | 10 | 10 | 50,000 | 782 |
-| 🚫 | 🚫 | 10,000 | 5 | 5 | 50,000 | 1349 |
-| 🚫 | 🚫 | 35,000 | 1 | 1 | 35,000 | 1287 |
-| 🚫 | ✅ | 500 | 30 | 6 | 3,000 | 282 |
-| 🚫 | ✅ | 1,500 | 10 | 2 | 3,000 | 387 |
-| 🚫 | ✅ | 3,000 | 5 | 1 | 3,000 | 920 |
-
-### Large to 16XL
-
-| Filters | RLS Usage | Concurrent Clients | DB Changes per second (total) | Records per second per client | Messages per second (total) | Latency p95 (ms) |
-| ------- | --------- | ------------------ | ----------------------------- | ----------------------------- | --------------------------- | ---------------- |
-| 🚫 | 🚫 | 500 | 64 | 64 | 32,000 | 184 |
-| 🚫 | 🚫 | 5,000 | 10 | 10 | 50,000 | 672 |
-| 🚫 | 🚫 | 10,000 | 5 | 5 | 50,000 | 1253 |
-| 🚫 | 🚫 | 35,000 | 1 | 1 | 35,000 | 1257 |
-| 🚫 | ✅ | 500 | 40 | 8 | 4,000 | 618 |
-| 🚫 | ✅ | 2,000 | 10 | 2 | 4,000 | 606 |
-| 🚫 | ✅ | 4,000 | 5 | 1 | 4,000 | 918 |
+
Don't forget to run your own benchmarks to make sure that the performance is acceptable for your use case.