26. Dez. 2025·7 Min. Lesezeit

Geteilte, veränderliche globale Zustände sicher durch expliziten Zustand ersetzen

Lerne, wie du geteilte, veränderliche Globals findest und durch request-scoped State ersetzt, um Race-Conditions und zufällige Fehler zu verhindern.

Geteilte, veränderliche globale Zustände sicher durch expliziten Zustand ersetzen

Warum geteilte, veränderliche Globals seltsame, zufällige Fehler verursachen

„Geteilt“ und „veränderlich“ klingt fancy, ist aber einfach: ein Wert lebt an einer Stelle (geteilt) und dein Code kann ihn ändern (veränderlich). Wenn viele Teile einer App denselben Wert lesen und schreiben, hängt das Verhalten vom Timing ab, nicht von der Absicht.

Deshalb wirken diese Fehler zufällig. Bei einem Nutzer, der klickt, sieht die App vielleicht in Ordnung aus. Unter parallelen Requests, Hintergrundjobs oder automatischen Retries können zwei Arbeiten gleichzeitig denselben Global berühren. Ein Request aktualisiert den Wert, und ein anderer Request verwendet ihn aus Versehen, obwohl es unterschiedliche Nutzer oder Aufgaben sind.

Ein typisches Beispiel ist eine Variable currentUser in einem Modul, ein „globaler Cache“, der eigentlich per-Request-Daten speichert, oder ein Singleton-Client, der stillschweigend Zustand hält (Header, Tokens, ein ausgewählter Tenant). Genau in diesen Fällen willst du geteilte, veränderliche Globals durch expliziten Zustand ersetzen.

Typische Symptome sehen so aus:

  • Nutzer sehen die Daten von jemand anderem oder sind als die falsche Person eingeloggt
  • „Funktioniert auf meinem Rechner“-Fehler, die nur unter Last auftreten
  • Flaky Tests, die je nach Reihenfolge bestehen oder fehlschlagen
  • Zufällige 401/403-Fehler, weil ein falsches Token wiederverwendet wurde
  • Hintergrundjobs, die die falsche Konfiguration verwenden

Das Ziel ist nicht „keine geteilten Dinge“. Datenbanken und Connection-Pools sind bewusst geteilt. Ziel ist: gemeinsame Ressourcen bleiben geteilt, während request-spezifischer Zustand übergeben oder pro Request erzeugt wird, sodass nichts stillschweigend wiederverwendet wird.

Das taucht besonders oft in AI-generierten Prototypen auf. FixMyMess stößt häufig auf versteckte Singletons, die harmlos wirken, bis echter Traffic kommt.

Was in echten Projekten als Global oder Singleton zählt

Ein „Global“ ist jeder Zustand, der außerhalb eines bestimmten Requests, Jobs oder Nutzer-Action lebt, aber während der Laufzeit gelesen und geschrieben wird. Ein „Singleton“ ist dasselbe mit dem netteren Label: „nur eine Instanz“, die von allen geteilt wird.

Im echten Code sind diese nicht immer offensichtlich. Sie verstecken sich in Modul-Level-Variablen, Framework-Services, statischen Klassenfeldern oder Helfern, die stillschweigend Dinge im Speicher behalten. Wenn du nach geteilten, veränderlichen Globals suchst, ist das die Form, die du finden willst.

Nicht alles Globale ist schlecht. Read-only Konstanten sind normalerweise sicher: Versionsstrings, feste Limits und Default-Settings. Das Risiko beginnt, wenn sich der Wert ändern kann, während Requests laufen. Mutable State plus Concurrency erzeugt die Bugs, die verschwinden, wenn du Logs hinzufügst.

Geteilter Zustand tritt oft so auf:

  • Modul-Level-Caches oder memoized Werte, die nutzer-spezifische Ergebnisse speichern
  • Ein geteiltes Config-Objekt, das mutiert wird (zum Beispiel „current tenant“)
  • Ein geteilter DB-Client-Wrapper, der auch per-Request-Daten wie currentUser speichert
  • Auth/Session-Helfer, die Tokens im Speicher halten statt pro Request
  • In-Memory-Queues oder Arrays mit „pending jobs“, die von mehreren Nutzern benutzt werden

Die Gefahr steigt mit dem Scale. Ein Single-User-Dev-Server kann in Ordnung aussehen, aber mehrere Worker oder Instanzen machen das Verhalten unvorhersehbar. Manche Fehler tauchen nur auf, wenn zwei Requests zeitlich überlappen.

Wenn du ein AI-generiertes Prototype geerbt hast, sind diese „eine Instanz“-Abkürzungen üblich. Bei Audits findet FixMyMess sie oft rund um Authentifizierung, Caching und Hintergrundarbeit.

Schnelle Anzeichen für versteckten geteilten Zustand

Versteckter geteilter Zustand zeigt sich meist als Probleme, die sich zufällig anfühlen. Die App „funktioniert größtenteils“, dann versagt sie auf eine Weise, die du nicht gezielt reproduzieren kannst.

Das klarste Symptom ist Datenüberlauf zwischen Nutzern. Ein Request aktualisiert etwas, und der nächste Request (von einem anderen Nutzer oder Tenant) sieht es. Du könntest bemerken, dass ein Nutzer plötzlich als jemand anders eingeloggt ist, der falsche Org-Name im Header erscheint oder ein Dashboard kurz die Daten eines anderen Kunden lädt.

Testverhalten ist ein weiterer Hinweis. Ein Test besteht, wenn du ihn allein ausführst, aber schlägt fehl, wenn du die gesamte Suite laufen lässt. Das bedeutet oft, dass ein Test Zustand hinterlässt (wie ein gecachter currentUser oder ein globaler DB-Wrapper mit veränderlichen Einstellungen), der den nächsten Test beeinflusst.

Traffic macht es schlimmer. Wenn Requests während eines Spitzenaufkommens überlappen, bekommst du gelegentliche 500-Fehler, die beim Retry verschwinden. Dieses Muster deutet häufig auf geteilte Objekte hin, die mid-request mutiert werden, wie ein globales Config-Objekt, ein Singleton-Client mit per-Request-Headern oder eine module-level current request-Variable.

Ein finales Indiz: „Es lief lokal.“ Viele Dev-Server bearbeiten Requests nacheinander, sodass Shared-State-Fehler erst in Produktion auftauchen, wenn mehrere Requests gleichzeitig laufen.

Schnelle rote Flaggen zum Scannen:

  • Eine Modul-Level-Variable, die während eines Requests verändert wird (token, tenantId, currentUser)
  • Ein Singleton-Client, der per-Request-Daten speichert (Header, Auth, Locale)
  • In-Memory-Caches, die als Quelle der Wahrheit dienen (nicht nur als Speed-Up)
  • Logs, die gemischte Request-IDs oder User-IDs im selben Flow zeigen
  • Bugs, die verschwinden, wenn du Print-Statements hinzufügst (Timing-Änderungen)

Teams bringen FixMyMess manchmal einen Prototyp, bei dem zwei Leute zur gleichen Zeit einloggen und Sessions sich kreuzen. Das ist fast immer Shared State, nicht „mysteriöse Auth“.

Wie du versteckte Singletons Schritt für Schritt aufspürst

Versteckte Singletons sind ein häufiger Grund für „zufälliges“ Verhalten unter Last: Ein Request ändert etwas, und der nächste erbt es.

Schritt-für-Schritt-Suche

Arbeite dich in dieser Reihenfolge durch den Code (spart Zeit und fängt die größten Übeltäter zuerst):

  • Suche nach Modul-Level-Variablen, die neu zugewiesen werden (nicht nur Konstanten). Achte auf Namen wie currentUser, token, client, config oder session.
  • Suche nach Singleton-Patterns: getInstance(), Kommentare wie „nur einmal erstellen“ oder lazy initialization wie „wenn noch nicht erstellt, erstelle jetzt“.
  • Untersuche Caches und Memoization. Ein Cache kann in Ordnung sein, aber er wird gefährlich, wenn der Key fehlt, zu breit ist (z. B. nur userId ohne tenantId) oder für alle Requests auf einen Default-Key zurückfällt.
  • Überprüfe Request-Handler und Middleware auf Objekte, die außerhalb des Handlers gespeichert werden. Eine übliche Falle: Ein Objekt einmal beim Startup erstellen und dann pro Request mutieren.
  • Checke Framework-„locals“ und app-weite Stores. Das Vermischen von request-locals und app-locals verwandelt per-Request-Daten in cross-request-Speicher.

Kurzer Realitätscheck

Um zu bestätigen, dass du den Übeltäter gefunden hast: öffne zwei Browser-Sessions (normal und incognito), logge dich als zwei verschiedene Nutzer ein und rufe denselben Endpoint ein paar Mal auf. Wenn Identitäten, Berechtigungen oder Einstellungen über die Sessions hinweg durchrutschen, hast du sehr wahrscheinlich noch geteilten, veränderlichen Zustand.

Ein einfaches Modell: Request-State vs. Shared Resources

Eine verlässliche Methode, geteilte, veränderliche Globals zu eliminieren, ist, alles in zwei Buckets zu trennen: Dinge, die zu einem Request gehören, und Dinge, die sicher über Requests geteilt werden können.

Per-Request-State sind alle Daten, die von Nutzer zu Nutzer oder Aufruf zu Aufruf variieren: die aktuelle User-ID, Auth-Claims, eine Correlation-ID fürs Logging, Locale und die konkrete Eingabe. Diese Daten dürfen nie in einer Modul-Level-Variable leben, weil zwei Requests überlappen und sich gegenseitig überschreiben können.

Shared Resources sind teure Bausteine, die du sicher wiederverwenden kannst: ein DB-Connection-Pool, ein HTTP-Client mit festen Einstellungen, ein kompiliertes Template. Entscheidend ist, dass diese Objekte keine per-Request-Daten in sich speichern.

Eine einfache Regel: Wenn es für Request B falsch wäre, das zu sehen, darf es nicht global sein.

Ein praktisches Modell, das dich ehrlich hält:

  • Lege per-Request-State in Funktionsparametern oder einem kleinen RequestContext-Objekt ab.
  • Baue Dependencies explizit mit Konstruktoren oder Factory-Funktionen.
  • Halte geteilte Objekte unveränderlich (Konfiguration) oder intern sicher (z. B. ein DB-Pool).
  • Wenn etwas geteilt und veränderlich bleiben muss, schütze es mit geeigneter Synchronisation.

Beispiel: Statt eines globalen currentUser erzeugst du ctx = { user, correlationId } beim Start des Requests und übergibst ctx an Handler und Services. Dein DB-Pool bleibt geteilt, aber Abfragefunktionen nehmen ctx entgegen, sodass Logging und Permissions korrekt bleiben.

Refactor-Plan: Von Globals zu explizitem Zustand

Sichere Caching pro Nutzer
Wir prüfen Caches und Keys, damit Nutzer- und Tenant-Daten nicht über Requests hinweg durchrutschen.

Um geteilte, veränderliche Globals sicher zu ersetzen, fang klein an. Wähle einen riskanten Bereich, bei dem falscher Zustand schnell schadet, z. B. Auth, Tenant-Auswahl oder Caching. Eine fokussierte Änderung ist leichter zu reviewen und weniger wahrscheinlich, andere Features zu brechen.

Schreibe zuerst auf, was der Code heimlich von „irgendwo“ zieht: current user, tenant ID, Feature Flags, Locale, Request ID usw. Erzeuge dann ein kleines Request-Context-Objekt, das nur das enthält, was wirklich gebraucht wird.

Eine Reihenfolge, die in den meisten Codebasen funktioniert:

  • Wähle einen Einstiegspunkt (API-Route, Handler oder Job) und baue dort den Request-Context.
  • Ändere eine Funktion nach der anderen so, dass sie den Context als Argument annimmt statt Globals zu lesen.
  • Wenn eine Funktion einen Service braucht (DB, Cache, Auth-Client), übergib ihn oder baue ihn aus einer Factory.
  • Behalte geteilte Ressourcen geteilt (z. B. Connection-Pool), aber halte per-Request-Daten getrennt.
  • Wenn es läuft, entferne das alte Global oder lass es laut fehlschlagen, falls darauf zugegriffen wird.

Eine Factory kann die Brücke sein, die einen großen Rewrite verhindert. Zum Beispiel kann createServices(ctx) authService, tenantService und auditLogger zurückgeben, die alle aus dem übergebenen Context lesen, nicht aus Modul-Level-Variablen. Das macht Abhängigkeiten sichtbar statt implizit.

Schließlich: Lösche oder freeze das alte Global. Lass es nicht „für alle Fälle“ herumliegen — jemand wird es sonst wieder benutzen.

Request-scoped Dependencies ohne Overengineering

Das Ziel ist simpel: Erzeuge pro Request, was nötig ist, übergib es explizit, und halte wirklich geteilte Teile (wie Connection-Pools) aus Sicht des Handlers read-only. Das reicht meist, um Concurrency-Bugs zu stoppen, ohne ein riesiges Dependency-System zu bauen.

Halte den Request-Container klein. Behandle ihn wie ein flaches Objekt, das nur variiert: current user, request ID, locale, feature flags und eine Uhr. Alles andere bleibt ein geteiltes Resource, das sicher wiederverwendet werden kann.

Ein praktisches Pattern in einer Web-App:

  • App-Startup: Erzeuge geteilte Ressourcen (DB-Pool, HTTP-Client, Logger-Config)
  • Pro Request: Erzeuge Request-State (User, Request-ID) und kleine Helfer, die ihn brauchen
  • Handler: Akzeptiere diese Dependencies als Parameter, nicht über Imports

Beispiel: Ein Login-Handler baut einmal RequestContext, und übergibt ihn dann an Services wie AuthService(ctx, db_pool, logger). Der DB-Pool ist geteilt, aber der Context nicht. So kann nicht der Zustand eines Nutzers in einen anderen Request „durchrutschen“, wenn zwei Requests gleichzeitig laufen.

Geteilte Dependencies, die meist sicher sind, umfassen einen DB-Connection-Pool (nicht eine einzelne Connection global gespeichert), einen HTTP-Client ohne per-User-Header und Logger-Config (aber nicht ein veränderliches currentUser-Global).

Hintergrundjobs sind eine Stelle, wo Leute gern zu Globals zurückkehren, weil es keinen Request gibt. Behandle Jobs genauso: Erstelle ein JobContext mit Job-ID und allen User-/Workspace-IDs, und übergib ihn an die Job-Funktion. Wenn du nur einen Wert weitergibst, übergib den Context.

Häufige Fehler, die das Problem verschlimmern

Behebe AI-Prototype-Fehler schnell
Verwandle ein AI-generiertes Prototype in zuverlässige, produktionsreife Software mit verifizierten Fixes.

Der schnellste Weg, deinen Refactor wieder kaputt zu machen, ist, das Global zu behalten und zu versuchen, es „bei jedem Request zurückzusetzen“. Das sieht in Single-User-Tests sicher aus, bricht aber, sobald zwei Requests überlappen. Wenn Request A den Wert zurücksetzt, während Request B ihn noch nutzt, entstehen schwer reproduzierbare Fehler.

Eine klassische Falle ist, currentUser in einer globalen Variable oder einem Singleton-Service zu speichern. Das fühlt sich bequem an, weil du von überall darauf zugreifen kannst, aber es macht jeden Request zur Race-Condition. Symptome sind Nutzer, die einander werden, Permissions, die umschalten, oder Audit-Logs mit falschem Actor.

Globale Caches sind ebenfalls riskant, wenn sie Tenant- oder User-Keys weglassen. Ein Cache, der nur eine Produkt-ID nutzt (oder schlimmer: einen einzigen „latest“-Wert), kann Daten über Accounts hinweg leaken. Der Bug sieht dann nicht nach Cache aus, sondern wie: „Meine App zeigt manchmal fremde Daten.“

Manche Fehler beginnen als Performance-Hacks und erzeugen dann Zuverlässigkeitsprobleme. Pro-Request neue DB-Conns statt eines Pools zu öffnen kann bei Last Connections erschöpfen. Dann „behebst“ du das mit Retries und Timeouts, was das eigentliche Problem versteckt und Fehler schwerer erklärbar macht.

Das Mischen von veränderlicher Config mit Runtime-State ist ein weiterer stiller Killer. Wenn du ein Config-Objekt zur Laufzeit änderst (Feature Flags, Base-URLs, Environment-Werte) und dieses Objekt geteilt ist, sieht jeder Request je nach Timing eine andere Konfiguration.

Schnelle rote Flaggen, die oft zusammen auftreten:

  • Ein Singleton hält Felder wie currentUser, token, requestId oder lastResult
  • Ein Cache-Key fehlt tenantId oder userId
  • „Reset“-Funktionen laufen in Middleware oder vor Handlern
  • DB-Connections werden im Handler geöffnet und geschlossen
  • Config-Objekte werden nach dem Startup geändert

Wenn du AI-generierten Code geerbt hast, tauchen diese Muster oft in Prototypen auf. Sie zeigen sich meist erst, wenn echte Nutzer die App gleichzeitig benutzen.

Wie du die Fixes mit einfachen Tests bestätigst

Nachdem du geteilte, veränderliche Globals ersetzt hast, fühlt sich der Code meist sofort besser an. Der wahre Beweis ist, dass er konsistent bleibt, wenn zwei Dinge gleichzeitig passieren.

1) Füge einen kleinen Concurrency-Test hinzu

Du brauchst keine riesige Test-Suite. Starte mit einem Test, der zwei Requests parallel abschickt mit verschiedenen Nutzern (oder verschiedenen API-Keys) und assertet, dass die Antworten nie vermischt werden.

# Pseudocode example
# Send two parallel requests:
# - user A logs in and fetches /me
# - user B logs in and fetches /me
# Assert A never sees B's data, and B never sees A's data.

Wenn dieser Test auch nur einmal fehlschlägt, steckt noch Shared State irgendwo.

2) Mach Cross-Talk mit Logs sichtbar

Füge einfache strukturierte Logs hinzu, die Request-ID und User-ID in jedem Handler und in jedem refaktorierten Service-Objekt enthalten. Scanne dann nach unmöglichen Sequenzen, z. B. Request-ID von User A loggt plötzlich User B.

Einige schnelle Checks, die versteckte Kopplungen aufdecken:

  • Führe die Test-Suite mit paralleler Ausführung aus.
  • Wiederhole den Concurrency-Test 50–200 Mal, um nondeterministische Fehler zu fangen.
  • Füge eine Assertion hinzu, dass request-scoped Objekte pro Request erzeugt werden (nicht wiederverwendet).
  • Überwache den Speicher während der Schleife, um zu bestätigen, dass es nach Entfernen von accidental Caches flach bleibt.
  • Verringere temporär Timeouts, damit Race-Conditions schneller sichtbar werden.

Wenn du weiterhin zufällige Fehler siehst, steckt meist noch ein Singleton (z. B. ein module-level Client, der currentUser speichert) oder ein Cache mit zu breitem Key dahinter.

Beispiel: Ein Prototype, das bricht, wenn zwei Nutzer einloggen

Ein häufiger AI-generierter Prototype-Bug wirkt harmlos in Single-User-Tests: Auth-Zustand wird in einer globalen Variable gehalten. Zum Beispiel speichert die App currentUser (oder ein accessToken) in einer Modul-Level-Variable, und alle API-Routen lesen davon.

In Produktion passiert Folgendes. Nutzer A loggt sich ein, dann Nutzer B kurz danach. Das globale currentUser wird überschrieben. Wenn Nutzer A nun auf „Mein Konto“ klickt, antwortet der Server manchmal als Nutzer B. Das wirkt zufällig, weil es vom Timing abhängt.

Typische Hinweise in Logs und Support-Tickets:

  • „Ich habe für eine Sekunde jemand anderes Daten gesehen“
  • Requests zeigen die falsche User-ID, obwohl Cookies korrekt aussehen
  • Das Problem tritt nur unter Last oder wenn zwei Personen gleichzeitig testen
  • Ein Refresh „behebt“ es manchmal

Die Lösung ist, das globale Singleton nicht mehr danach zu fragen, wer der aktuelle Nutzer ist. Baue stattdessen pro Request ein Auth-Service auf, das auf explizitem Context (Header, Cookies, Session-ID) basiert. Jeder Request bekommt sein eigenes auth-Objekt und Handler erhalten dieses als Parameter.

Konkret: Parse das Token aus dem eingehenden Request, verifiziere es, und übergib den verifizierten Nutzer an die Funktionen, die ihn brauchen. Geteilte Ressourcen (wie ein DB-Pool) können geteilt bleiben, aber die Nutzer-Identität muss request-scoped sein.

Nach diesem Refactor werden parallele Logins und API-Aufrufe konsistent: Nutzer A sieht immer Nutzer A’s Daten, auch wenn Nutzer B aktiv ist.

Schnelle Checkliste vor dem Shipping

Sichere dein AI-generiertes Backend
Wir härten Sicherheitslücken, die oft in AI-generiertem Code zu finden sind, inklusive offengelegter Secrets.

Bevor du deployst, mach einen letzten Pass, um sicherzugehen, dass du keinen geteilten Zustand übersehen hast.

Ship-Ready Sanity-Checks

  • Scanne Request-Handling-Code nach Modul-Level-Variablen, die sich ändern (alles, was während eines Requests geschrieben wird).
  • Überprüfe Caches und Memoization. Stelle sicher, dass Keys das trennen, was Nutzer und Tenants unterscheidet (und Locale, wenn die Ausgabe variiert).
  • Bestätige, dass Request-Context übergeben wird, nicht aus versteckten Singletons gelesen wird. Wenn eine Funktion die aktuelle Nutzer-, Auth-Token-, Tenant- oder Zeitzonen-Info braucht, sollte sie sie als Argument bekommen (oder über eine request-scoped Dependency).
  • Auditier, was du teilst. Connection-Pools, unveränderliche Config und read-only Clients sind meist in Ordnung. Alles mit veränderlichen Feldern (wie currentUser, lastQuery, headers) ist es nicht.
  • Führe einen parallelen Test aus: zwei Nutzer loggen sich ein und machen gleichzeitig verschiedene Aktionen. Suche nach Cross-User-Daten, gemischten Sessions oder „zufälligen“ Berechtigungsfehlern.

Wenn du ein AI-generiertes Prototype geerbt hast, lohnt sich diese Checkliste doppelt. Solche Apps schleichen oft globale currentUser-State oder einzelne geteilte Clients mit veränderlichen Headern ein.

Nächste Schritte, wenn dein AI-generierter Code Concurrency-Bugs hat

Wenn deine App mit einem Nutzer funktioniert, aber unter realer Last versagt, behandle es als Shared-State-Problem, bis das Gegenteil bewiesen ist. Viele AI-generierte Projekte behalten Daten im Speicher, die eigentlich in einen Request, eine Session oder eine Datenbank gehören.

Beginne mit einer schnellen Inventur aller Dinge, die von mehr als einem Request geschrieben werden können. Suche nach Modul-Level-Variablen, gecachten Objekten, die Nutzerdaten halten, beim Startup erstellten Singletons und Utility-Funktionen mit internem Zustand.

Ein einfacher Weg voranzukommen ist, eine Nutzerreise End-to-End zu reparieren. Wähle den Pfad, der am häufigsten bricht (Login, Checkout, Dateiupload). Refaktoriere nur diesen Pfad so, dass Zustand durch Funktionsargumente oder request-scoped Dependencies fließt. Wenn ein Pfad sauber ist, lassen sich die Muster leichter und sicherer wiederholen.

Wenn du nicht sicher bist, wo das versteckte Singleton ist, eng die Suche ein:

  • Füge Logs für Objekt-IDs und User-IDs an Schlüsselstellen hinzu (Auth, DB-Zugriff, Caching)
  • Grep nach „global“, „singleton“, „cache“, „memo“, "static" und Modul-Level-Zuweisungen
  • Deaktiviere temporär In-Memory-Caching, um zu sehen, ob der Bug verschwindet

Manchmal ist ein Experten-Review schneller, besonders wenn der Code mehrere Frameworks, Hintergrundjobs und Custom-Auth mischt. FixMyMess (FixMyMess, fixmymess.ai) diagnostiziert und repariert AI-generierten Code, beginnend mit einer kostenlosen Code-Audit. Die meisten Projekte werden innerhalb von 48–72 Stunden mit AI-unterstützten Tools und menschlicher Verifikation abgeschlossen, sodass die Fixes unter Last halten.

Häufige Fragen

Was genau ist ein „shared mutable global“?

Ein "shared mutable global" ist jeder Wert, der außerhalb eines konkreten Requests oder Jobs lebt und zur Laufzeit verändert werden kann. Er wird gefährlich, wenn mehrere Requests ihn lesen und schreiben können, weil dann die Arbeit eines Nutzers den Zustand eines anderen überschreiben kann.

Warum verursachen geteilte Globals Fehler, die sich zufällig anfühlen?

Weil das Ergebnis von Timing abhängt, nicht nur vom Codefluss. Bei parallelen Requests, Retries oder Hintergrundaufgaben können zwei Operationen sich überschneiden und denselben Zustand wiederverwenden, sodass der Fehler je nach Last und Scheduling kommt und geht.

Was sind typische reale Beispiele für versteckte Globals oder Singletons?

Achte auf Dinge wie ein module-level currentUser, einen Singleton-Client, der Header oder Tokens speichert, einen "globalen Cache", der per-User-Ergebnisse hält, oder ein geteiltes Config-Objekt, das mutiert wird (z. B. „current tenant“). Diese Muster funktionieren in Single-User-Tests, scheitern aber, wenn Requests überlappen.

Was sind die klarsten Anzeichen dafür, dass Zustand zwischen Requests leakt?

Wenn Nutzer das falsche Konto, den falschen Tenant-Namen oder Daten eines anderen Nutzers sehen, behandle das als Shared-State-Problem, bis das Gegenteil bewiesen ist. Flaky Tests, die von der Ausführungsreihenfolge abhängen, und gelegentliche 401/403-Fehler sind ebenfalls starke Indizien.

Was ist eine schnelle Methode, das Problem zu reproduzieren oder zu bestätigen?

Öffne zwei Sessions (z. B. normales Fenster und privates Fenster), logge dich als zwei verschiedene Nutzer ein und rufe dieselben Endpunkte wiederholt auf. Wenn Identität, Berechtigungen oder Einstellungen jemals übergehen, liegt irgendwo noch request-spezifischer Zustand im geteilten Speicher.

Was darf geteilt werden und was niemals?

Geteilte Ressourcen sind in Ordnung, solange sie keine per-Request-Daten enthalten. Ein DB-Connection-Pool, ein HTTP-Client mit festen Einstellungen und unveränderliche Konfiguration sind normalerweise sicher; gefährlich wird es, wenn das geteilte Objekt veränderliche Felder wie currentUser, token oder tenantId enthält.

Wie ersetze ich ein globales `currentUser`, ohne alles neu zu schreiben?

Erzeuge an der Eintrittsstelle (Handler oder Middleware) ein kleines Request-Context-Objekt, das nur per-Request-Informationen enthält (z. B. User-Identität und Request-ID). Übergebe dieses Context-Objekt oder daraus abgeleitete Services an Funktionen, statt auf ein globales Objekt zuzugreifen, das stillschweigend Zustand trägt.

Wie behalte ich Caching bei, ohne dass Daten zwischen Nutzern durchlaufen?

Caching ist in Ordnung, wenn es als Performance-Layer dient, nicht als versteckte Wahrheit. Wichtig sind korrekte Scope-Keys: Füge Tenant- und User-Information hinzu, wenn sich Ergebnisse pro Tenant/User unterscheiden, und vermeide einen einzelnen "latest"-Wert, den jeder Request überschreiben kann.

Wie soll ich Hintergrundjobs behandeln, wenn es keinen „Request“ gibt?

Behandle Jobs wie Requests: Erzeuge ein JobContext mit Job-ID und allen Workspace-/User-IDs, die zum Job gehören, und übergebe es an die Job-Funktion. Vermeide das Lesen oder Schreiben von module-level Zustand in Job-Runnern, weil mehrere Jobs gleichzeitig laufen können.

Was soll ich tun, wenn ich ein AI-generiertes Prototype geerbt habe, das unter Last scheitert?

Beginne mit einem fokussierten Audit rund um Auth, Caching und Singleton-Clients — das sind häufige Fehlerquellen in AI-generierten Prototypen. Wenn du willst, führt FixMyMess eine kostenlose Code-Audit durch, identifiziert den versteckten Shared State und repariert den Code mit menschlicher Verifikation; die meisten Projekte sind in 48–72 Stunden fertig. (FixMyMess, fixmymess.ai)