Race Conditions in CRUD‑Apps können doppelte Bestellungen und falsche Summen verursachen. Lerne übliche Kollisionspunkte und Lösungen mit Constraints, Sperren und UX‑Schutzmaßnahmen.

Eine Race Condition entsteht, wenn zwei (oder mehr) Requests fast gleichzeitig dieselben Daten ändern und das Endergebnis vom Timing abhängt. Jeder Request für sich sieht korrekt aus. Zusammen liefern sie ein falsches Resultat.
Ein einfaches Beispiel: Zwei Personen klicken innerhalb einer Sekunde auf "Speichern" an derselben Kundenakte. Die eine ändert die E‑Mail, die andere die Telefonnummer. Wenn beide den kompletten Datensatz senden, kann der zweite Schreibvorgang den ersten überschreiben, und eine Änderung verschwindet ohne Fehler.
Das passiert häufiger in schnellen Apps, weil Nutzer mehr Aktionen pro Minute auslösen. Es tritt auch bei Lastspitzen auf: Flash‑Sales, Monatsabschluss, große E‑Mail‑Kampagnen oder immer dann, wenn mehrere Requests dieselben Zeilen treffen.
Nutzer melden selten "eine Race Condition". Sie melden Symptome: doppelte Bestellungen oder Kommentare, fehlende Updates ("Ich hab gespeichert, aber es ist wieder anders"), seltsame Summen (Lagerbestand wird negativ, Zähler springen zurück) oder Status, die unerwartet umschalten (genehmigt, dann wieder offen).
Retries verschlimmern das Problem. Leute doppelklicken, aktualisieren nach einer langsamen Antwort, senden von zwei Tabs oder erleben instabiles Netz, das Browser und mobile Apps Requests erneut abschickt. Wenn der Server jede Anfrage als frischen Schreibvorgang behandelt, entstehen doppelte Creates, doppelte Zahlungen oder doppelte Statusänderungen, die nur einmal stattfinden sollten.
CRUD‑Apps wirken einfach: Eine Zeile lesen, ein Feld ändern, speichern. Der Haken ist, dass deine App das Timing nicht kontrolliert. Datenbank, Netzwerk, Retries, Hintergrundarbeit und Nutzerverhalten überlappen sich.
Ein häufiger Auslöser ist, dass zwei Personen dieselbe Aufzeichnung bearbeiten. Beide laden dieselben "aktuellen" Werte, beide machen gültige Änderungen, und der letzte Save überschreibt stillschweigend den ersten. Niemand hat etwas falsch gemacht, aber ein Update ging verloren.
Es kann auch mit einer Person passieren. Ein Doppelklick auf den Speichern‑Button, Hin‑und‑her‑Tippen oder eine langsame Verbindung, die jemanden zum erneuten Drücken von Senden bringt, können denselben Schreibvorgang zweimal senden. Wenn der Endpunkt nicht idempotent ist, entstehen Duplikate, doppelte Abbuchungen oder ein Status springt zwei Schritte vor.
Moderne Nutzung bringt mehr Überschneidungen. Mehrere Tabs oder Geräte mit dem gleichen Konto können widersprüchliche Updates senden. Hintergrundjobs (E‑Mails, Abrechnung, Sync, Aufräumarbeiten) können dieselben Zeilen wie Webrequests berühren. Automatische Retries am Client, Load Balancer oder Job‑Runner können eine Anfrage wiederholen, die bereits erfolgreich war.
Wenn du Features schnell auslieferst, wird dieselbe Zeile oft von mehr Stellen aktualisiert, als sich jemand erinnert. Wenn du ein chat‑getriebenes Builder‑Tool wie Koder.ai verwendest, wächst die App noch schneller — behandle Concurrency also als normalen Zustand, nicht als Randfall.
Race Conditions tauchen selten in "Erstelle‑Datensatz"‑Demos auf. Sie treten dort auf, wo zwei Requests fast gleichzeitig dieselbe Wahrheit berühren. Wer die üblichen Hotspots kennt, kann von Anfang an sichere Schreibvorgänge entwerfen.
Alles, was sich wie "einfach +1" anfühlt, kann unter Last kaputtgehen: Likes, Views, Totals, Rechnungs‑ oder Ticketnummern. Das riskante Muster ist: Wert lesen, erhöhen, zurückschreiben. Zwei Requests können denselben Ausgangswert lesen und einander überschreiben.
Workflows wie Entwurf -> Eingereicht -> Genehmigt -> Bezahlt wirken simpel, aber Kollisionen sind häufig. Problematisch wird es, wenn gleichzeitig mehrere Aktionen möglich sind (genehmigen und bearbeiten, stornieren und zahlen). Ohne Schutz kann ein Datensatz Schritte überspringen, zurückspringen oder in verschiedenen Tabellen unterschiedliche Zustände zeigen.
Behandle Statusänderungen wie einen Vertrag: Erlaube nur den nächsten gültigen Schritt und lehne alles andere ab.
Verbleibende Plätze, Lagerbestände, Termine und "verfügbare Kapazität" führen zum klassischen Oversell‑Problem. Zwei Käufer checken gleichzeitig aus, beide sehen Verfügbarkeit und beide schaffen es. Wenn die Datenbank nicht das letzte Wort hat, verkaufst du irgendwann mehr, als du hast.
Manche Regeln sind absolut: eine E‑Mail pro Konto, ein aktives Abo pro Nutzer, ein offener Warenkorb pro Nutzer. Diese Regeln scheitern oft, wenn du zuerst prüfst ("existiert schon eine?") und dann insertest. Unter Konkurrenz können beide Requests die Prüfung bestehen.
Wenn du CRUD‑Flows schnell erzeugst (zum Beispiel per Chat mit Koder.ai), notiere diese Hotspots früh und stütze sie mit Constraints und sicheren Schreibvorgängen, nicht nur UI‑Checks.
Viele Race Conditions beginnen mit etwas Banalem: dieselbe Aktion wird zweimal gesendet. Nutzer doppelklicken. Das Netz ist langsam, also klicken sie erneut. Ein Telefon registriert zwei Taps. Manchmal ist es unabsichtlich: die Seite aktualisiert sich nach einem POST und der Browser bietet an, das Formular erneut zu senden.
Dann kann dein Backend zwei Creates oder Updates parallel ausführen. Wenn beide erfolgreich sind, erhältst du Duplikate, falsche Summen oder einen Statuswechsel, der zweimal ausgeführt wird (z. B. Genehmigung und nochmalige Genehmigung). Es wirkt zufällig, weil es vom Timing abhängt.
Die sicherste Herangehensweise ist Verteidigung in der Tiefe. Verbessere die UI, aber geh davon aus, dass die UI versagt.
Praktische Änderungen für die meisten Schreibflüsse:
Beispiel: Ein Nutzer tippt auf dem Handy zweimal "Rechnung bezahlen". Die UI sollte den zweiten Tap blocken. Der Server sollte beim zweiten Request den gespeicherten Idempotency‑Key sehen und die ursprüngliche Erfolgsmeldung zurückgeben, statt doppelt zu belasten.
Statusfelder wirken simpel, bis zwei Dinge sie gleichzeitig ändern wollen. Ein Nutzer klickt Genehmigen, während ein automatischer Job denselben Datensatz als Abgelaufen markiert, oder zwei Teammitglieder bearbeiten denselben Eintrag in unterschiedlichen Tabs. Beide Updates können erfolgreich sein, aber der finale Status hängt vom Timing ab, nicht von deinen Regeln.
Behandle Status als kleinen Zustandsautomaten. Halte eine kurze Tabelle erlaubter Moves (zum Beispiel: Entwurf -> Eingereicht -> Genehmigt und Eingereicht -> Abgelehnt). Dann prüft jeder Schreibvorgang: "Ist dieser Wechsel vom aktuellen Status aus erlaubt?" Wenn nicht, lehne ihn ab, anstatt stillschweigend zu überschreiben.
Optimistisches Locking hilft, veraltete Updates zu erkennen, ohne andere Nutzer zu blockieren. Füge eine Versionsnummer (oder updated_at) hinzu und verlange, dass sie beim Speichern übereinstimmt. Wenn jemand anders die Zeile nach dem Laden geändert hat, wirkt dein Update auf null Zeilen und du kannst eine klare Meldung anzeigen wie "Dieser Eintrag hat sich geändert, aktualisiere bitte und versuche es erneut."
Ein einfaches Muster für Status‑Updates ist:
Halte Statusänderungen an einer Stelle zusammen. Wenn Updates über Bildschirme, Hintergrundjobs und Webhooks verstreut sind, verpasst du Regeln. Packe sie hinter eine einzige Funktion oder Endpoint, der dieselben Übergangsprüfungen überall durchsetzt.
Der häufigste Zählerbug wirkt harmlos: Die App liest einen Wert, addiert 1 und schreibt ihn zurück. Unter Last lesen zwei Requests dieselbe Zahl und beide schreiben denselben neuen Wert — eine Erhöhung geht verloren. Das fällt leicht durch die Tests, weil es meistens funktioniert.
Wenn ein Wert nur inkrementiert oder dekrementiert wird, lass die Datenbank das in einer Anweisung tun. Dann wendet die DB Änderungen sicher an, auch wenn viele Requests gleichzeitig kommen.
UPDATE posts
SET like_count = like_count + 1
WHERE id = $1;
Dasselbe gilt für Inventar, View‑Counts, Retry‑Zähler und alles, was sich als "neu = alt + delta" ausdrücken lässt.
Totals gehen oft kaputt, wenn du eine abgeleitete Zahl speicherst (order_total, account_balance, project_hours) und sie an mehreren Stellen aktualisierst. Wenn du die Summe aus Quellzeilen (Positionszeilen, Buchungseinträge) berechnen kannst, vermeidest du eine ganze Klasse von Drift‑Bugs.
Wenn du ein Total aus Performance‑Gründen speichern musst, behandle es als kritischen Schreibvorgang. Aktualisiere Quellzeilen und gespeichertes Total in derselben Transaktion. Sorge dafür, dass nur ein Writer dieselbe Total‑Zeile gleichzeitig ändern kann (Locking, guarded updates oder ein einziger Owner‑Pfad). Füge Constraints hinzu, die unmögliche Werte verhindern (z. B. Lagerbestand >= 0). Rekonsilliere gelegentlich mit einem Hintergrundjob, der neu berechnet und Abweichungen markiert.
Konkretes Beispiel: Zwei Nutzer fügen gleichzeitig Artikel zum selben Warenkorb hinzu. Wenn jede Anfrage cart_total liest, ihren Artikelpreis addiert und zurückschreibt, kann eine Addition verschwinden. Wenn du Warekorb‑Items und cart_total zusammen in einer Transaktion aktualisierst, bleibt das Total auch bei vielen parallelen Klicks korrekt.
Wenn du weniger Race Conditions willst, fang in der Datenbank an. App‑Code kann retryen, timeouts setzen oder zweimal laufen. Eine Datenbank‑Constraint ist das letzte Tor, das auch dann korrekt bleibt, wenn zwei Requests gleichzeitig kommen.
Unique‑Constraints stoppen Duplikate, die "nie passieren sollten": E‑Mail‑Adressen, Bestellnummern, Rechnungs‑IDs oder die Regel "eine aktive Subscription pro Nutzer". Wenn zwei Anmeldungen gleichzeitig reinkommen, akzeptiert die DB eine Zeile und lehnt die andere ab.
Foreign Keys verhindern gebrochene Referenzen. Ohne sie kann ein Request einen Eltern‑Datensatz löschen, während ein anderer ein Kind anlegt, das auf nichts zeigt — verwaiste Zeilen, die später schwer zu reinigen sind.
Check‑Constraints halten Werte in sicheren Bereichen und erzwingen einfache Zustandsregeln. Zum Beispiel: quantity >= 0, rating zwischen 1 und 5, oder Status auf eine erlaubte Menge begrenzt.
Behandle Constraint‑Fehler als erwartete Outcomes, nicht als "Serverfehler". Fange Unique‑, FK‑ und Check‑Verstöße ab, gib eine klare Meldung wie "Diese E‑Mail wird bereits verwendet" und logge Details zur Fehleranalyse, ohne Interna zu leaken.
Beispiel: Zwei Leute klicken während Lags zweimal auf "Bestellung erstellen". Mit einem Unique‑Constraint auf (user_id, cart_id) bekommst du keine zwei Bestellungen, sondern eine und eine saubere, erklärbare Ablehnung.
Manche Schreibvorgänge bestehen nicht aus einer einzigen Anweisung. Du liest eine Zeile, prüfst eine Regel, änderst einen Status und fügst vielleicht ein Audit‑Log hinzu. Wenn zwei Requests das gleichzeitig tun, können beide die Prüfung bestehen und beide schreiben. Das ist das klassische Fehlermuster.
Kapsle den mehrstufigen Schreibvorgang in einer Datenbanktransaktion, sodass alle Schritte zusammen erfolgreich sind oder keiner. Viel wichtiger: Die Transaktion gibt dir einen Ort, um zu kontrollieren, wer dieselben Daten gleichzeitig ändern darf.
Wenn nur ein Akteur eine Zeile zurzeit ändern darf, nutze ein Row‑Level‑Lock. Zum Beispiel: Sperre die Bestellzeile, bestätige, dass sie noch im "pending"‑Zustand ist, wechsle sie zu "approved" und schreibe den Audit‑Eintrag. Der zweite Request wartet, prüft erneut und bricht ab.
Wähle nach Häufigkeit von Kollisionen:
Halte Sperrzeiten kurz. Mach so wenig wie möglich während einer Sperre: keine externen API‑Aufrufe, keine langsame Dateiarbeit, keine großen Schleifen. Wenn du Flows in einem Tool wie Koder.ai baust, halte die Transaktion nur für die DB‑Schritte, den Rest machst du nach dem Commit.
Wähle einen Flow, bei dem ein Kollisionsfehler Geld oder Vertrauen kosten würde. Ein typischer ist: Bestellung erstellen, Lager reservieren, dann Bestellstatus auf bestätigt setzen.
Schreibe genau auf, welche Schritte dein Code heute ausführt, in welcher Reihenfolge. Sei spezifisch: was wird gelesen, was geschrieben und was bedeutet "Erfolg"? Kollisionen verstecken sich in der Lücke zwischen Lesen und späterem Schreiben.
Ein Härtungspfad, der in den meisten Stacks funktioniert:
Füge einen Test hinzu, der die Fixes beweist. Starte zwei Requests gleichzeitig gegen dasselbe Produkt und dieselbe Menge. Behaupte, dass genau eine Bestellung bestätigt wird und die andere kontrolliert scheitert (kein negativer Bestand, keine doppelten Reservierungszeilen).
Wenn du Apps schnell generierst (auch mit Plattformen wie Koder.ai), lohnt sich diese Checkliste für die wenigen Schreibpfade, die am wichtigsten sind.
Einer der größten Ursachen ist Vertrauen in die UI. Deaktivierte Buttons und Client‑Checks helfen, aber Nutzer können doppelklicken, refreshen, zwei Tabs öffnen oder eine Anfrage aus einer instabilen Verbindung erneut abspielen. Wenn der Server nicht idempotent ist, rutschen Duplikate durch.
Ein weiterer stiller Bug: Du fängst einen DB‑Fehler (z. B. Unique‑Violation) ab, machst aber trotzdem im Workflow weiter. Das führt oft zu Situationen wie "Create ist fehlgeschlagen, aber wir haben trotzdem die E‑Mail gesendet" oder "Zahlung fehlgeschlagen, aber wir haben die Bestellung trotzdem als bezahlt markiert." Sobald Side‑Effects passieren, ist es schwer, das zurückzudrehen.
Lange Transaktionen sind auch eine Falle. Wenn du eine Transaktion offen hältst, während du Mail, Payments oder Drittanbieter‑APIs aufrufst, hältst du Sperren länger als nötig. Das erhöht Wartezeiten, Timeouts und die Chance, dass Requests sich gegenseitig blockieren.
Das Mischen von Hintergrundjobs und Benutzeraktionen ohne eine einzige Quelle der Wahrheit schafft Split‑Brain‑Zustände. Ein Job retryt und updated eine Zeile, während ein Nutzer sie bearbeitet — und beide glauben, der letzte Schreiber gewesen zu sein.
Einige "Fixes", die es nicht wirklich richten:
Wenn du mit einem Chat‑to‑App‑Tool wie Koder.ai baust, gelten dieselben Regeln: Fordere Server‑seitige Constraints und klare transaktionale Grenzen, nicht nur schönere UI‑Guards.
Race Conditions treten oft erst unter realer Last auf. Ein Pre‑Ship‑Pass kann die häufigsten Kollisionspunkte ohne großen Rewrite aufdecken.
Fang mit der Datenbank an. Wenn etwas einzigartig sein muss (E‑Mails, Rechnungsnummern, eine aktive Subscription pro Nutzer), mach es zu einer echten Unique‑Constraint, nicht zu einer App‑Ebene "wir prüfen vorher"‑Regel. Stell dann sicher, dass dein Code erwartet, dass die Constraint manchmal fehlschlägt und eine klare, sichere Antwort liefert.
Als Nächstes: Status. Jede Statusänderung (Entwurf -> Eingereicht -> Genehmigt) sollte gegen eine explizite Menge erlaubter Übergänge validiert werden. Wenn zwei Requests denselben Datensatz verschieben, sollte der zweite abgewiesen werden oder ein No‑Op sein, nicht einen Zwischenzustand erzeugen.
Eine praktische Pre‑Release‑Checkliste:
Wenn du Flows in Koder.ai baust, mache diese Punkte zu Akzeptanzkriterien: Die generierte App sollte bei Wiederholungen und Concurrency sicher fehlschlagen, nicht nur den Happy Path bedienen.
Zwei Mitarbeiter öffnen dieselbe Purchase‑Request. Beide klicken innerhalb weniger Sekunden auf Genehmigen. Beide Requests erreichen den Server.
Was schiefgehen kann, ist unordentlich: Die Anfrage wird zweimal "genehmigt", zwei Benachrichtigungen gehen raus und an Genehmigungen geknüpfte Summen (Budgetverbrauch, tägliche Genehmigungszählung) steigen um 2. Beide Updates sind für sich gültig, aber sie kollidieren.
Hier ein Plan, der mit einer PostgreSQL‑artigen DB gut funktioniert.
Füge eine Regel hinzu, die garantiert, dass nur ein Genehmigungs‑Eintrag pro Request existieren kann. Beispielsweise Genehmigungen in einer separaten Tabelle speichern und einen Unique‑Constraint auf request_id setzen. Dann schlägt der zweite Insert fehl, selbst wenn die App‑Logik buggy ist.
Beim Genehmigen erledige den gesamten Wechsel in einer Transaktion:
Kommt der zweite Mitarbeiter zu spät, sieht er entweder 0 geänderte Zeilen oder einen Unique‑Constraint‑Fehler. In beiden Fällen gewinnt nur eine Änderung.
Nach der Änderung sieht der erste Mitarbeiter "Genehmigt" und bekommt die normale Bestätigung. Der zweite Mitarbeiter sieht eine freundliche Meldung wie: "Diese Anfrage wurde bereits von jemand anderem genehmigt. Bitte aktualisieren, um den aktuellen Status zu sehen." Kein Endlos‑Spinner, keine doppelten Benachrichtigungen, keine stillen Fehler.
Wenn du einen CRUD‑Flow in einer Plattform wie Koder.ai generierst (Go‑Backend mit PostgreSQL), kannst du diese Prüfungen einmal in die Approve‑Action einbauen und das Muster für andere "nur ein Gewinner"‑Aktionen wiederverwenden.
Race Conditions lassen sich am besten beheben, wenn du sie zur wiederholbaren Routine machst, nicht zur einmaligen Fehlerjagd. Konzentriere dich auf die wenigen Schreibpfade, die am meisten zählen, und mache sie langweilig korrekt, bevor du etwas anderes polierst.
Fang damit an, deine Top‑Kollisionspunkte zu benennen. In vielen CRUD‑Apps sind es dieselben drei: Zähler (Likes, Inventar, Salden), Statusänderungen (Entwurf -> Eingereicht -> Genehmigt) und doppelte Übermittlungen (Doppelklick, Retries, langsames Netz).
Eine Routine, die sich bewährt hat:
Wenn du auf Koder.ai baust, ist der Planning Mode ein praktischer Ort, um jeden Schreibflow als Schritte und Regeln zu skizzieren, bevor du Code für Go und PostgreSQL generierst. Snapshots und Rollback sind nützlich, wenn du neue Constraints oder Sperr‑Verhalten auslieferst und schnell zurück willst, falls ein Edge‑Case auftaucht.
Mit der Zeit wird das zur Gewohnheit: Jede neue Schreibfunktion bekommt eine Constraint, einen Transaktionsplan und einen Concurrency‑Test. So werden Race Conditions in CRUD‑Apps keine Überraschungen mehr.