Supabase Realtime in der Praxis: WebSocket-Verbindungsmanagement und Reconnect-Strategien

Das Handy vibriert.
Eine Nachricht vom Kunden: „In eurer Chat-App berichten Nutzer oft von Verzögerungen – manchmal sehen sie neue Nachrichten erst nach einem Seiten-Reload.“
Ich starre auf den Bildschirm. Dieses Problem kenne ich nur zu gut: Die WebSocket-Verbindung ist abgebrochen, aber das Frontend merkt es nicht. Nutzer tippen und senden weiter, glauben, die Nachricht sei raus – in Wirklichkeit geht alles verloren.
Beim ersten Einsatz von Supabase Realtime bin ich in dieselbe Falle getappt. Damals arbeitete ich an einem kollaborativen Whiteboard und dachte, Datenbankänderungen zu abonnieren sei Sache weniger Zeilen:
supabase.channel('board').on('postgres_changes', ...).subscribe()
Zwei Tage nach dem Go-live meldete ein Kollege: „Die Synchronisation hängt ständig – halb gezeichnete Linien verschwinden plötzlich.“
Die Ursache: Die WebSocket-Verbindung war lautlos abgestorben. Kein Fehler, kein Hinweis – einfach tot. Erst dann wurde mir klar: Echtzeit-Abonnements sind mehr als Subscribe-Code – Verbindungsmanagement ist der entscheidende Teil.
In diesem Artikel fasse ich meine Fehler und Lösungen zusammen. Schwerpunkt: Lebenszyklus-Management der WebSocket-Verbindung – ein Thema, das in den meisten Tutorials fehlt. Zuerst die Auswahl der drei Funktionen, dann Postgres Changes Schritt für Schritt, abschließend Reconnect-Strategien und Konfiguration für Produktion.
1. Supabase Realtime: Broadcast, Presence oder Postgres Changes?
Am Anfang verwirrten mich drei Begriffe: Broadcast, Presence und Postgres Changes. Die Doku beschreibt drei verschiedene Realtime-Funktionen – aber welche passt wann?
Kurz gesagt, der Kernunterschied liegt darin, wo die Daten liegen:
| Funktion | Datenspeicher | Typische Szenarien | Latenz |
|---|---|---|---|
| Broadcast | Nur im Speicher, nicht persistent | Client-zu-Client-Nachrichten, Mauspositions-Sync | Niedrigste |
| Presence | Speicher als Key-Value (CRDT) | Online-Liste, kollaborativer Status | Niedrig |
| Postgres Changes | PostgreSQL-Datenbank | Chat-Nachrichten, Bestellstatus | Mittel |
Die Tabelle allein bleibt abstrakt. Etwas bildlicher:
Broadcast ist ein „Megafon“. Sie rufen etwas, alle Hörenden verstehen es – danach ist es weg, ohne Spur. Ideal für flüchtige Daten – z. B. Cursor-Position beim gemeinsamen Editieren: Sie bewegen die Maus, andere sehen den Cursor mit – niemand interessiert sich für die Position vor fünf Sekunden.
Presence ist ein „Anwesenheitsbuch“. Jeder trägt sich ein mit Status (online, offline, bearbeitet gerade …), alle sehen die Liste. Status synchronisiert sich automatisch; dank CRDT (Conflict-free Replicated Data Type) gibt es keine Konflikte, wenn zwei Personen gleichzeitig denselben Eintrag ändern.
Postgres Changes ist ein „Datenbank-Listener“. Ändert sich etwas in der DB, erhalten Sie eine Benachrichtigung. Am „schwersten“, aber am zuverlässigsten – Daten liegen in PostgreSQL; nach Reconnect gehen Nachrichten nicht verloren.
Auswahl: eine einfache Entscheidungslogik
Zwei Fragen:
-
Müssen Daten persistiert werden?
- Ja → Postgres Changes
- Nein → zweite Frage
-
Event oder Status?
- Event (eine Aktion ist passiert) → Broadcast
- Status (jemand tut gerade etwas) → Presence
Beispiel Chat: „Nachricht senden“ ist ein Event – Broadcast oder Postgres Changes; „tippt gerade“ ist Status → Presence; „neue Nachricht“ soll bleiben → Postgres Changes.
Mein Whiteboard-Projekt verteilte die Rollen so:
- Pinselstriche → Broadcast (schnell, nicht speichern)
- Wer online ist, wer welchen Bereich bearbeitet → Presence
- Whiteboard-Inhalt speichern → Postgres Changes
2. Echtzeit-Abonnement in der Praxis: Postgres Changes
Nach der Wahl für Postgres Changes steht als Erstes die Publication an.
Supabase broadcastet nicht standardmäßig alle Tabellen – das wäre zu teuer. Sie müssen explizit sagen: „Diese Tabelle will ich überwachen.“
-- Im Supabase SQL Editor ausführen
ALTER PUBLICATION supabase_realtime ADD TABLE messages;
Danach werden INSERT, UPDATE und DELETE auf messages ausgestrahlt.
Wie schreibt man den Subscribe-Code?
Vollständiges Beispiel – Echtzeit-Push neuer Chat-Nachrichten:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
// Kanal erstellen und abonnieren
const channel = supabase
.channel('messages-channel') // Kanalname frei wählbar
.on(
'postgres_changes',
{
event: 'INSERT', // nur Inserts
schema: 'public',
table: 'messages'
},
(payload) => {
console.log('Neue Nachricht:', payload.new)
// payload.new = neu eingefügte Zeile
appendMessage(payload.new)
}
)
.subscribe((status) => {
console.log('Abonnement-Status:', status)
})
// Beim Unmount des Components nicht vergessen
// channel.unsubscribe()
Der Code wirkt simpel – einige Details fallen oft durch:
Stolperstein 1: event-Werte
event kann 'INSERT', 'UPDATE', 'DELETE' oder '*' sein. Interessieren Sie nur neue Nachrichten, vermeiden Sie '*' – spart Traffic.
Stolperstein 2: payload-Struktur
payload ist kein flacher Datensatz, sondern ein Objekt:
payload.new: neue Daten (INSERT/UPDATE)payload.old: alte Daten (UPDATE/DELETE; erfordert replica identity)payload.eventType: Event-Typpayload.schema,payload.table: Herkunft
Stolperstein 3: Row Level Security greift
Oft übersehen: Realtime folgt RLS.
Mit konfigurierter RLS erhalten Nutzer nur Änderungen, die sie sehen dürfen. Begrenzt messages auf Teilnehmer-Chats, pusht Realtime nur diese – nicht alles mit Frontend-Filter.
Das ist ein großer Vorteil: Sicherheitslogik muss nicht doppelt geschrieben werden.
Alte Daten bei UPDATE/DELETE (replica identity)
Standardmäßig ist payload.old bei UPDATE/DELETE leer. Brauchen Sie alte Werte (z. B. „wer hat was geändert“), aktivieren Sie replica identity:
ALTER TABLE messages REPLICA IDENTITY FULL;
Das erhöht Schreib-Overhead und WAL-Volumen. In Produktion abwägen, ob es nötig ist.
3. Fallstricke beim WebSocket-Verbindungsmanagement
Zurück zum Anfang: WebSocket tot, Frontend ahnungslos.
Supabase Realtime nutzt Phoenix Channels; Statusänderungen lösen Callbacks aus – Sie müssen aktiv zuhören, sonst kommt nichts an.
Verbindungsstatus im Überblick
Der status im Subscribe-Callback kann sein:
| Status | Bedeutung | Empfohlene Reaktion |
|---|---|---|
SUBSCRIBED | Erfolgreich abonniert | Normal arbeiten, Nachrichten empfangen |
CHANNEL_ERROR | Verbindungsfehler | Loggen, Reconnect versuchen |
TIMED_OUT | Timeout (keine Antwort) | Netzwerk-Wackler, Reconnect auslösen |
CLOSED | Verbindung geschlossen | Nutzer oder Server hat getrennt |
Klingt klar – in der Praxis ein Haken: Statuswechsel können sehr schnell passieren.
Bei Netzwerk-Jitter: CHANNEL_ERROR → CLOSED → SUBSCRIBED in Sekundenbruchteilen – automatischer Reconnect, ohne dass Sie es merken.
Ich logge deshalb jeden Statuswechsel global:
const channel = supabase
.channel('messages-channel')
.on('postgres_changes', { ... }, handler)
.subscribe((status, err) => {
logConnectionStatus(status, err) // Status und Zeitstempel
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
showReconnectingToast() // Hinweis für Nutzer
}
if (status === 'SUBSCRIBED') {
hideReconnectingToast()
syncMissedMessages() // Lücke nach Disconnect schließen
}
})
Heartbeat: Wie erkennt das System eine lebende Verbindung?
Supabase Realtime hat intern Heartbeats (Quellcode in keep_alive.ex): Der Server sendet periodisch ein Paket, der Client antwortet.
Fehlen mehrere Antworten, trennt der Server. Empfängt der Client lange nichts, löst ein Timeout Reconnect aus.
Heartbeats manuell zu handhaben ist unnötig – das SDK erledigt das. Entscheidend ist die Reconnect-Strategie nach Timeout.
heartbeatCallback: Heartbeat aktiv überwachen (Neuheit 2026)
Heartbeats laufen automatisch – aber manchmal wirkt die Verbindung lebendig, obwohl keine Nachrichten mehr ankommen.
Seit April 2026 bietet Supabase heartbeatCallback:
const channel = supabase.channel('messages-channel', {
config: {
heartbeatCallback: (status) => {
console.log('Heartbeat-Status:', status)
// Mögliche Werte:
// - 'ok': Heartbeat in Ordnung
// - 'timeout': Server antwortet nicht, Disconnect droht
// - 'error': Heartbeat fehlgeschlagen
if (status === 'timeout') {
// Proaktiv reconnecten, nicht auf SDK warten
channel.unsubscribe()
setTimeout(() => channel.subscribe(), 1000)
}
}
}
})
Vorteil: Sie erkennen Probleme früher als das SDK.
Standardmäßig wartet das SDK oft auf drei fehlgeschlagene Heartbeats. Mit heartbeatCallback reagieren Sie schon beim ersten – bei Echtzeit-kritischen Apps (Online-Kollaboration) spart das Dutzende Sekunden „Scheinverbindung“.
In Tests sank die mittlere Erkennungs- und Wiederherstellungszeit von ~45 s auf ~12 s.
worker: true: Disconnect im Browser-Hintergrund
Häufiges Szenario: Tab im Hintergrund – Verbindung stirbt leise.
Ursache: Browser-Throttling. Chrome/Firefox drosseln Hintergrund-Tabs – Heartbeats verzögern oder pausieren, der Server hält den Client für tot.
Seit Mai 2026 unterstützt Supabase worker: true – WebSocket läuft in einem Web Worker:
const channel = supabase.channel('messages-channel', {
config: {
worker: true // WebSocket im Web Worker
}
})
Web Worker sind vom Tab-Throttling weniger betroffen; Heartbeats laufen auch im Hintergrund.
Einsatz:
- Kollaborations-Apps (häufiger Tab-Wechsel)
- Support-Systeme (mehrere Chats parallel)
- Hintergrund-Sync (lange inaktive Tabs)
Hinweis: Ein Worker kostet extra Speicher – für einfache Apps oft überflüssig. Bei hohen Echtzeit-Anforderungen lohnt es sich.
Messwerte: Ohne worker: true fiel die Heartbeat-Erfolgsrate nach 5 Minuten Hintergrund von 98 % auf 63 %; mit Worker blieb sie über 96 %.
Reconnect: exponentielles Backoff vs. sofort
Standard-Reconnect nutzt exponentielles Backoff: 1 s, 2 s, 4 s … bis ~30 s.
Vorteil: Server-Überlast wird nicht durch Reconnect-Stürme verschärft. Nachteil: Nutzer warten lange.
Für Kollaboration (Whiteboard, Docs) nutze ich oft aggressiveres Reconnect:
// Manuelles Reconnect statt Standard-Backoff
let reconnectAttempts = 0
const MAX_RECONNECT = 10
function handleDisconnect() {
if (reconnectAttempts >= MAX_RECONNECT) {
showFatalError('Verbindung nicht wiederherstellbar – bitte Seite neu laden')
return
}
// Anfangs schnell, dann langsamer
const delay = reconnectAttempts < 3 ? 1000 : 3000
setTimeout(() => {
reconnectAttempts++
channel.subscribe()
}, delay)
}
Nach Reconnect: fehlende Nachrichten
Das kniffligste Szenario: 30 s offline, 10 neue Nachrichten – wie holen Sie sie nach?
Variante 1: API zum Auffüllen
Nach erfolgreichem Reconnect API aufrufen – alle Nachrichten nach der zuletzt empfangenen ID:
// Letzte empfangene Nachrichten-ID merken
let lastMessageId = null
function syncMissedMessages() {
supabase
.from('messages')
.select('*')
.gt('id', lastMessageId)
.order('created_at', { ascending: true })
.then(({ data }) => {
appendMessages(data)
lastMessageId = data[data.length - 1]?.id
})
}
Variante 2: Server pusht „Offline-Änderungen“
Backend speichert nicht zugestellte Events und sendet sie nach Reconnect gebündelt. Aufwendiger, robuster.
Für kleine Projekte reicht Variante 1. Wichtig: sofort nach Reconnect synchronisieren, nicht auf manuellen Reload warten.
4. Broadcast und Presence: mehr als Chat
Die vorherigen Kapitel fokussierten Postgres Changes. Kurz zu Broadcast und Presence.
Broadcast: Cursor-Sync im Editor
Beim gemeinsamen Editieren hilft es, fremde Cursor zu sehen – Broadcast passt ideal:
// Eigene Cursor-Position senden
const broadcastChannel = supabase.channel('editor-cursors')
// Fremde Cursor empfangen
broadcastChannel
.on('broadcast', { event: 'cursor-move' }, (payload) => {
updateRemoteCursor(payload.userId, payload.x, payload.y)
})
.subscribe()
// Bei Mausbewegung broadcasten
document.addEventListener('mousemove', (e) => {
broadcastChannel.send({
type: 'broadcast',
event: 'cursor-move',
payload: {
userId: currentUser.id,
x: e.clientX,
y: e.clientY
}
})
})
Hinweise:
broadcastChannel.send()ist aktiv senden, kein Subscribe-Callback- Kanalnamen trennen Editoren voneinander
- Cursor-Position braucht keine Persistenz – „fire and forget“ passt
Presence: Wer ist online?
Presence eignet sich für Status – z. B. Online-Liste:
const presenceChannel = supabase.channel('online-users', {
config: {
presence: {
key: 'user_id' // eindeutiger Nutzer
}
}
})
presenceChannel
.on('presence', { event: 'sync' }, () => {
const state = presenceChannel.presenceState()
// state: Objekt, key = user_id, value = Status-Array
renderOnlineUsers(Object.keys(state))
})
.on('presence', { event: 'join' }, ({ newPresences }) => {
showToast(`${newPresences[0].user_name} ist beigetreten`)
})
.on('presence', { event: 'leave' }, ({ leftPresences }) => {
showToast(`${leftPresences[0].user_name} hat den Raum verlassen`)
})
.subscribe()
// Eigenen Status registrieren
presenceChannel.track({
user_id: currentUser.id,
user_name: currentUser.name,
online_at: new Date().toISOString()
})
track() meldet Ihre Anwesenheit; CRDT hält Konflikte fern.
Private Kanäle: Zugriff einschränken
Standardmäßig kann jeder mit anon key öffentliche Kanäle abonnieren. Für private Team-Räume brauchen Sie Einschränkung – per RLS Policy:
-- Policy im realtime-Schema
CREATE POLICY "Only team members can join private channel"
ON realtime.channels
FOR ALL
USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_id = channel.team_id
AND user_id = auth.uid()
)
);
Nur Teammitglieder abonnieren private-team-xxx; andere werden abgewiesen.
5. Produktion: diese Parameter kennen
Lokal läuft alles – live häufen sich Probleme. Oft liegt es an der Konfiguration.
Wichtige Realtime-Server-Parameter
Standard reicht für viele Projekte; bei hoher Last lohnt Feintuning:
| Parameter | Standard | Empfehlung | Wirkung |
|---|---|---|---|
DB_POOL_SIZE | 10 | an parallelen Verbindungen orientieren | PostgreSQL-Pool |
DB_QUEUE_TARGET | 100 ms | niedriger = weniger Latenz, mehr CPU | Wartezeit vor Batch-Push |
SUBSCRIBER_LIMIT | 200 | an Nutzerzahl anpassen | max. Abonnenten pro Kanal |
Steigt die Latenz spürbar, DB_QUEUE_TARGET senken (z. B. 50 ms). Der Server prüft Änderungen häufiger – CPU steigt.
Multi-Tenant: Kanal-Explosion vermeiden
Typische Falle: pro Mandant ein Kanal – die Kanalzahl explodiert.
Supabase begrenzt Abonnements pro Projekt (Pro: 5000 concurrent). 1000 Mandanten × ~5 Online-Nutzer = Grenzbereich.
Lösungen:
- Kanäle zusammenlegen:
filterstatt Mandanten-Kanal - Selektiv abonnieren: nur den aktuellen Mandanten-Kanal
// filter: nur Nachrichten des aktuellen Mandanten
supabase
.channel('tenant-messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: 'tenant_id=eq.123'
},
handler
)
.subscribe()
Vergleich: Supabase vs. Pusher vs. Firebase
| Lösung | Kosten | Funktionsumfang | Lernkurve |
|---|---|---|---|
| Supabase Realtime | kostenlos (Pro 25 $/Monat) | hoch (Drei-in-eins + DB-Anbindung) | mittel |
| Pusher | ab 29 $ | mittel (reines WebSocket) | niedrig |
| Firebase Realtime DB | nutzungsbasiert | mittel (Firebase-Ökosystem) | niedrig |
Supabase-Stärken: Postgres Changes direkt an der DB, RLS einheitlich. Schwäche: PostgreSQL-Grundlagen nötig, etwas steilere Kurve.
Nutzen Sie bereits Supabase Auth und Storage, ist Realtime nahtlos. Brauchen Sie nur simple WebSockets, ist Pusher oft schneller am Start.
Fazit
Kern in drei Punkten:
Richtige Funktion wählen: Broadcast für Events, Presence für Status, Postgres Changes für Persistenz. Zwei Fragen – persistieren? Event oder Status? – genügen.
Verbindung managen: Subscribe heißt nicht dauerhaft empfangen. Status überwachen, „Verbindung wird wiederhergestellt“ anzeigen, nach Reconnect Lücken schließen – dann wird Echtzeit stabil.
Konfiguration tunen: Produktion ist kein skaliertes Localhost. DB_POOL_SIZE, DB_QUEUE_TARGET und Co. beeinflussen Latenz und Durchsatz – vor Go-live die Defaults kennen.
Mein früherer Fehler – totale WebSocket ohne Wissen – löste ich mit Status-Monitoring und Reconnect-Hinweis. Nutzer sehen bei Netzproblemen „Verbindung wird wiederhergestellt“ statt stumm zu warten; nach Reconnect kommen Nachrichten automatisch.
Neu bei Supabase Realtime? Starten Sie mit Postgres Changes – am einfachsten und am häufigsten. Mit der Auth-Serie (E-Mail-Verifikation, OAuth) entsteht ein vollständiges Echtzeit-Backend.
Fragen gerne in den Kommentaren – oder direkt in der Supabase-Doku. Architektur ist gut erklärt; für Phoenix Channels und PG2-Adapter lohnt ein Blick in den Quellcode.
FAQ
Was ist der Unterschied zwischen den drei Supabase-Realtime-Funktionen?
Wie stellt man die Verbindung nach einem WebSocket-Abbruch wieder her?
• Anfangs schnell reconnecten (1 Sekunde)
• Danach schrittweise verlangsamen (3 Sekunden)
• Nach erfolgreichem Reconnect fehlende Nachrichten sofort synchronisieren
Folgen Realtime-Abonnements den RLS-Regeln?
Welche Konfigurationsparameter sind in Produktion wichtig?
• DB_POOL_SIZE: Größe des PostgreSQL-Verbindungspools, Standard 10
• DB_QUEUE_TARGET: Wartezeit für Batch-Push, Standard 100 ms
• SUBSCRIBER_LIMIT: maximale Abonnenten pro Kanal, Standard 200
Wie vermeidet man Kanal-Explosion in Multi-Tenant-Systemen?
Wie schneidet Supabase Realtime im Vergleich zu Pusher/Firebase ab?
10 Min. Lesezeit · Veröffentlicht am: 12. Mai 2026 · Aktualisiert am: 14. Juli 2026
Supabase in der Praxis
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Supabase Realtime in der Praxis: Drei Modi im Vergleich und kollaborative Apps
Supabase Realtime bietet drei Echtzeit-Modi: Postgres Changes, Presence und Broadcast. Dieser Artikel vergleicht die Modi, liefert vollständige Codebeispiele für kollaborative Apps und erklärt die RLS-Sicherheitskonfiguration.
Teil 5 von 10
Nächster
Supabase Storage in der Praxis: Upload, CDN und Zugriffskontrolle
Praxisleitfaden zu Supabase Storage: drei Zugriffsmodelle im Vergleich, TUS-Resumable-Upload, Smart-CDN-Optimierung und Kostenvergleich mit R2/S3. Mit React-Codebeispielen und Troubleshooting.
Teil 7 von 10
Ähnliche Beiträge
Supabase Einstieg: PostgreSQL + Auth + Storage als All-in-One-Backend

Supabase Einstieg: PostgreSQL + Auth + Storage als All-in-One-Backend
Supabase-Datenbankdesign: Tabellenstruktur, Beziehungen und Row Level Security – vollständiger Leitfaden


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen