Kotlin-Netzwerke bei langsamen Verbindungen: Timeouts und sichere Wiederholungen
Praktische Kotlin-Netzwerkstrategien für langsame Verbindungen: Timeouts setzen, sicher cachen, ohne Duplikate wiederholen und kritische Aktionen bei instabilen Mobilnetzen schützen.

Was bei langsamen und instabilen Verbindungen schiefgeht
Auf Mobilgeräten bedeutet „langsam“ meist nicht „keine Internetverbindung“. Meist ist die Verbindung nutzbar, aber nur in kurzen Intervallen. Eine Anfrage kann 8 bis 20 Sekunden dauern, unterwegs hängenbleiben und dann doch noch fertig werden. Oder sie gelingt kurz und schlägt beim nächsten Versuch fehl, weil das Telefon von WLAN auf LTE gewechselt hat, in einen Funklochbereich geraten ist oder das OS die App in den Hintergrund geschickt hat.
„Instabil“ ist noch schlimmer. Pakete fallen aus, DNS-Abfragen laufen in Zeitüberschreitung, TLS-Handshakes schlagen fehl und Verbindungen werden zufällig zurückgesetzt. Man kann im Code alles „richtig" machen und trotzdem Feldfehler sehen, weil sich das Netzwerk unter einem ändert.
Genau hier versagen häufig die Default-Einstellungen. Viele Apps verlassen sich auf Bibliotheksstandards für Timeouts, Retries und Caching, ohne zu entscheiden, was für echte Nutzer „gut genug" ist. Defaults sind oft für stabiles WLAN und schnelle APIs optimiert, nicht für die S-Bahn, einen Aufzug oder ein belebtes Café.
Nutzer beschreiben nicht „Socket-Timeouts" oder „HTTP 503“. Sie bemerken Symptome: endlose Ladeindikatoren, plötzliche Fehler nach langem Warten (und beim nächsten Versuch funktioniert es), doppelte Aktionen (zwei Buchungen, zwei Bestellungen, doppelte Abbuchungen), verlorene Aktualisierungen und gemischte Zustände, bei denen die UI „fehlgeschlagen" anzeigt, der Server aber tatsächlich erfolgreich war.
Langsame Netze machen kleine Designlücken zu Geld- und Vertrauensproblemen. Wenn die App nicht klar zwischen „wird gesendet", „fehlgeschlagen" und „fertig" trennt, tippen Nutzer erneut. Wenn der Client blind retryt, können Duplikate entstehen. Unterstützt der Server keine Idempotenz, kann eine wackelige Verbindung mehrere „erfolgreiche" Schreibvorgänge erzeugen.
„Kritische Aktionen" sind alles, was höchstens einmal passieren darf und korrekt sein muss: Zahlungen, Checkout-Abschlüsse, Terminbuchungen, Punkteübertragungen, Passwortänderungen, Speichern einer Lieferadresse, Einreichen eines Anspruchs oder das Senden einer Freigabe.
Ein realistisches Beispiel: Jemand sendet den Checkout bei schwachem LTE. Die App schickt die Anfrage, dann bricht die Verbindung ab, bevor die Antwort ankommt. Der Nutzer sieht einen Fehler, tippt erneut auf „Bezahlen" und nun erreichen zwei Anfragen den Server. Ohne klare Regeln kann die App nicht entscheiden, ob sie erneut senden, warten oder stoppen soll. Der Nutzer weiß nicht, ob er es noch einmal versuchen soll.
Regeln festlegen, bevor Sie Code anpassen
Bei langsamen oder instabilen Verbindungen entstehen die meisten Fehler durch unklare Regeln, nicht durch den HTTP-Client. Bevor Sie Timeouts, Caching oder Retries anfassen, schreiben Sie auf, was „korrekt" für Ihre App bedeutet.
Beginnen Sie mit Aktionen, die niemals zweimal ausgeführt werden dürfen. Das sind meist Geld- und Kontoaktionen: Bestellung aufgeben, Karte belasten, Auszahlung anstoßen, Passwort ändern, Account löschen. Wenn ein Nutzer zweimal tippt oder die App retryt, sollte der Server es dennoch als eine Anfrage behandeln. Wenn Sie das noch nicht garantieren können, behandeln Sie diese Endpunkte als „kein automatischer Retry" bis Sie es können.
Entscheiden Sie als Nächstes, was jeder Bildschirm bei schlechtem Netzwerk tun darf. Einige Bildschirme können offline nützlich sein (letztes Profil, frühere Bestellungen). Andere sollten nur lesbar oder mit einem klaren „nochmals versuchen"-Status fungieren (Inventarstände, Live-Preise). Gemischte Erwartungen führen zu verwirrender UI und riskantem Caching.
Legen Sie akzeptable Wartezeiten pro Aktion fest, basierend darauf, wie Nutzer denken, nicht darauf, was im Code schön aussieht. Login toleriert kurzere Wartezeiten. Datei-Uploads brauchen länger. Checkout sollte sich schnell, aber auch sicher anfühlen. Ein 30-Sekunden-Timeout mag auf dem Papier „zuverlässig" sein und sich dennoch gebrochen anfühlen.
Schließlich entscheiden Sie, was Sie auf dem Gerät speichern und wie lange. Cache-Daten helfen, aber veraltete Daten können zu falschen Entscheidungen führen (alte Preise, abgelaufene Berechtigungen).
Schreiben Sie die Regeln irgendwo hin, wo alle sie finden (ein README reicht). Halten Sie es einfach:
- Welche Endpunkte dürfen nicht dupliziert werden und erfordern Idempotenz-Handling?
- Welche Bildschirme müssen offline funktionieren, welche sind im Offline-Modus nur lesbar?
- Was ist die maximale Wartezeit pro Aktion (Login, Feed-Refresh, Upload, Checkout)?
- Was darf auf dem Gerät gecacht werden und wie lange ist die Ablaufzeit?
- Nach einem Fehler: zeigen Sie eine Fehlermeldung, legen Sie es in eine Warteschlange oder verlangen Sie manuellen Retry?
Sobald diese Regeln klar sind, werden Timeout-Werte, Caching-Header, Retry-Policy und UI-Zustände viel einfacher zu implementieren und zu testen.
Timeouts, die den Erwartungen der Nutzer entsprechen
Langsame Netze schlagen auf verschiedene Arten fehl. Eine gute Timeout-Konfiguration wählt nicht einfach eine Zahl, sie passt zu dem, was der Nutzer gerade tun will, und schlägt schnell genug fehl, damit die App sich erholen kann.
Die drei Timeouts in einfachen Worten:
- Connect-Timeout: wie lange Sie warten, um eine Verbindung zum Server aufzubauen (DNS-Lookup, TCP, TLS). Scheitert das, hat die Anfrage im Grunde nie gestartet.
- Write-Timeout: wie lange Sie warten, während Sie den Request-Body senden (Uploads, große JSON, langsamer Uplink).
- Read-Timeout: wie lange Sie warten, bis der Server nach dem Senden Daten zurückschickt. Das zeigt sich oft in instabilen Mobilnetzen.
Timeouts sollten Bildschirm und Risiko widerspiegeln. Ein Feed kann langsamer laden ohne echten Schaden. Eine kritische Aktion sollte entweder klar abgeschlossen oder klar fehlgeschlagen sein, damit der Nutzer entscheiden kann, was als Nächstes zu tun ist.
Ein praktischer Ausgangspunkt (nach Messungen anpassen):
- Listen (geringes Risiko): connect 5–10s, read 20–30s, write 10–15s.
- Search-as-you-type: connect 3–5s, read 5–10s, write 5–10s.
- Kritische Aktionen (hoch riskant, z. B. „Bezahlen" oder „Bestellung abschicken"): connect 5–10s, read 30–60s, write 15–30s.
Konsistenz ist wichtiger als Perfektion. Wenn der Nutzer nach dem Tippen auf „Absenden" zwei Minuten einen Spinner sieht, tippt er erneut.
Vermeiden Sie „endloses Laden" auch in der UI, indem Sie eine klare Obergrenze setzen. Zeigen Sie sofort Fortschritt, erlauben Sie Abbruch, und nach (z. B.) 20–30 Sekunden zeigen Sie „Versuchen wir es weiter…" mit Optionen zum Wiederholen oder zur Verbindungskontrolle. So bleibt die Erfahrung ehrlich, auch wenn die Netzwerkbibliothek noch wartet.
Wenn ein Timeout passiert, loggen Sie genug, um Muster später zu debuggen, aber ohne Geheimnisse zu protokollieren. Nützliche Felder sind Pfad der URL (nicht die vollständige Query), HTTP-Methode, Status (falls vorhanden), Timing-Aufschlüsselung (connect vs write vs read, falls verfügbar), Netztyp (WLAN, Mobilfunk, Flugmodus), ungefähre Request/Response-Größe und eine Anfrage-ID, damit Client-Logs und Server-Logs verknüpft werden können.
Ein einfaches, konsistentes Kotlin-Netzwerk-Setup
Bei langsamen Verbindungen werden kleine Inkonsistenzen im Client-Setup zu großen Problemen. Eine saubere Basis hilft beim schnelleren Debuggen und gibt jeder Anfrage dieselben Regeln.
Ein Client, eine Policy
Beginnen Sie mit einem zentralen Ort, an dem Sie Ihren HTTP-Client bauen (häufig ein OkHttpClient, den Retrofit verwendet). Legen Sie die Basics dort fest, damit jede Anfrage gleich verhält: Standard-Header (App-Version, Locale, Auth-Token) und ein klarer User-Agent, Timeouts an einer Stelle gesetzt (nicht über die Aufrufe verstreut), Debug-Logging und eine einzelne Retry-Entscheidung (auch wenn es „keine automatischen Retries" ist).
Hier ein kleines Beispiel, das die Konfiguration in einer Datei zusammenhält:
val okHttp = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.callTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.build()
chain.proceed(request)
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttp)
.addConverterFactory(MoshiConverterFactory.create())
.build()
Zentrales Fehler-Mapping, das zu Nutzertexten führt
Netzwerkfehler sind nicht einfach „eine Ausnahme". Wenn jeder Bildschirm sie anders behandelt, bekommen Nutzer zufällige Meldungen.
Erstellen Sie einen Mapper, der Fehler in eine kleine Menge nutzerfreundlicher Outcomes übersetzt: keine Verbindung/Flugmodus, Timeout, Serverfehler (5xx), Validierungs- oder Auth-Fehler (4xx) und ein unbekannter Fallback.
So bleibt die UI-Sprache konsistent („Keine Verbindung" vs. „Erneut versuchen") ohne technische Details durchscheinen zu lassen.
Requests taggen und abbrechen, wenn Bildschirme schließen
Bei instabilen Netzen können Calls spät fertig werden und einen bereits geschlossenen Bildschirm aktualisieren. Machen Sie das Abbrechen zur Standardregel: wenn ein Bildschirm geschlossen wird, stoppt seine Arbeit.
Mit Retrofit und Kotlin-Coroutines bricht das Schließen des Coroutine-Scopes (z. B. im ViewModel) den zugrunde liegenden HTTP-Call ab. Bei nicht-Coroutine-Aufrufen behalten Sie eine Referenz auf das Call-Objekt und rufen cancel() auf. Sie können Requests auch taggen und Gruppen von Calls abbrechen, wenn ein Feature verlassen wird.
Hintergrundarbeit darf nicht von der UI abhängen
Alles, was unbedingt abgeschlossen werden muss (Report senden, Queue synchronisieren, eine Einreichung fertigstellen), sollte in einem Scheduler laufen, der dafür gedacht ist. Auf Android ist WorkManager die übliche Wahl, weil er später wiederholen und App-Neustarts überleben kann. Halten Sie UI-Aktionen leichtgewichtig und übergeben Sie längere Arbeiten an Hintergrundjobs, wenn es sinnvoll ist.
Caching-Regeln, die auf Mobilgeräten sicher sind
Caching kann auf langsamen Verbindungen viel bringen, weil es Wiederhol-Downloads reduziert und Bildschirme sofort erscheinen lässt. Es kann aber auch problematisch sein, wenn veraltete Daten zur falschen Zeit angezeigt werden, z. B. ein altes Kontoguthaben oder eine veraltete Lieferadresse.
Ein sicherer Ansatz ist, nur das zu cachen, was der Nutzer tolerieren kann, wenn es etwas älter ist, und für alles, was Geld, Sicherheit oder eine finale Entscheidung betrifft, frische Prüfungen zu erzwingen.
Verlässliche Cache-Control-Basics
Die meisten Regeln lassen sich auf wenige Header reduzieren:
max-age=60: die gecachte Antwort darf 60 Sekunden lang ohne Serverabfrage wiederverwendet werden.no-store: diese Antwort darf gar nicht gespeichert werden (am besten für Tokens und sensible Bildschirme).must-revalidate: ist die Antwort abgelaufen, muss vor der Nutzung mit dem Server geprüft werden.
Auf Mobilgeräten verhindert must-revalidate stille Fehlinformationen nach einer Offline-Zeit. Wenn der Nutzer die App nach einer U-Bahn-Fahrt öffnet, wollen Sie einen schnellen Bildschirm, aber die App soll auch bestätigen, was noch stimmt.
ETag-Refreshes: schnell, günstig und zuverlässig
Für Leseendpunkte sind ETag-basierte Validierungen oft besser als lange max-age-Werte. Der Server sendet ein ETag mit der Antwort. Beim nächsten Mal sendet die App If-None-Match mit diesem Wert. Wenn sich nichts geändert hat, antwortet der Server mit 304 Not Modified, was klein und schnell ist — ideal für schwache Netze.
Das funktioniert gut für Produktlisten, Profildetails und Einstellungsbildschirme.
Eine einfache Faustregel:
- Cache-Leseendpunkte mit kurzem
max-ageplusmust-revalidateund unterstützen SieETag, wo möglich. - Cachen Sie keine Schreibendpunkte (POST/PUT/PATCH/DELETE). Behandeln Sie sie immer als netzgebunden.
- Verwenden Sie
no-storefür sensible Daten (Auth-Antworten, Zahlungsabläufe, private Nachrichten). - Statische Assets (Icons, öffentliche Konfiguration) können länger gecacht werden, da das Risiko veralteter Daten gering ist.
Treffen Sie Caching-Entscheidungen konsistent in der gesamten App. Nutzer bemerken Inkonsistenzen eher als kleine Verzögerungen.
Sichere Retries, ohne alles schlimmer zu machen
Retries wirken wie eine einfache Lösung, können aber nach hinten losgehen. Retryen Sie die falschen Requests, und Sie erzeugen zusätzliche Last, verbrauchen Akku und lassen die App hängen.
Beginnen Sie damit, nur Fehler erneut zu versuchen, die wahrscheinlich vorübergehend sind. Eine abgebrochene Verbindung, ein Read-Timeout oder ein kurzer Serverausfall können beim nächsten Versuch klappen. Ein falsches Passwort, ein fehlendes Feld oder ein 404 tun das nicht.
Eine praktische Regel:
- Retryen Sie Timeouts und Verbindungsfehler.
- Retryen Sie 502, 503 und manchmal 504.
- Retryen Sie keine 4xx (außer 408 oder 429, falls Sie eine klare Warte-Regel haben).
- Retryen Sie keine Requests, die bereits den Server erreicht haben und dort verarbeitet werden könnten.
- Halten Sie Retries niedrig (meist 1 bis 3 Versuche).
Backoff + Jitter: weniger Retry-Stürme
Wenn viele Nutzer auf denselben Ausfall treffen, können sofortige Retries eine Welle von Traffic erzeugen, die die Erholung verlangsamt. Verwenden Sie exponentielles Backoff (längere Wartezeit bei jedem Versuch) und fügen Sie Jitter (kleine zufällige Verzögerung) hinzu, damit Geräte nicht synchron retryen.
Beispiel: warten Sie ca. 0,5 Sekunden, dann 1 Sekunde, dann 2 Sekunden, mit einem zufälligen +/- 20 %.
Setzen Sie eine Obergrenze für die gesamte Retry-Zeit
Ohne Limits können Retries Nutzer in einen Spinner fangen. Wählen Sie eine maximale Gesamtzeit für die gesamte Operation, inklusive Wartezeiten. Viele Apps zielen auf 10 bis 20 Sekunden, bevor sie stoppen und eine klare Option zum erneuten Versuch zeigen.
Stimmen Sie das auch auf den Kontext ab. Wenn jemand ein Formular absendet, möchte er schnell eine Antwort. Wenn ein Hintergrund-Sync scheitert, können Sie später erneut versuchen.
Retryen Sie niemals automatisch nicht-idempotente Aktionen (wie eine Bestellung aufgeben oder eine Zahlung senden), es sei denn, Sie haben Schutz wie einen Idempotency-Key oder serverseitige Duplikatprüfungen. Wenn Sie die Sicherheit nicht garantieren können, schlagen Sie klar fehl und lassen Sie den Nutzer entscheiden, was zu tun ist.
Duplikatsvermeidung bei kritischen Aktionen
Bei langsamer oder instabiler Verbindung tippen Nutzer oft zweimal. Das OS kann im Hintergrund erneut versuchen. Ihre App kann nach einem Timeout erneut senden. Wenn die Aktion etwas erstellt (Bestellung, Geld senden, Passwort ändern), können Duplikate schaden.
Idempotenz bedeutet, dass dieselbe Anfrage dasselbe Ergebnis liefern sollte. Wird die Anfrage wiederholt, sollte der Server die ursprüngliche Antwort zurückgeben oder sagen „bereits erledigt".
Verwenden Sie einen Idempotency-Key für jeden kritischen Versuch
Bei kritischen Aktionen erzeugen Sie beim Start des Versuchs einen eindeutigen Idempotency-Key und senden ihn mit der Anfrage (oft als Header wie Idempotency-Key oder als Feld im Body).
Ein praktischer Ablauf:
- Erzeugen Sie eine UUID als Idempotency-Key, wenn der Nutzer auf „Bezahlen" tippt.
- Speichern Sie den Schlüssel lokal mit einem kleinen Datensatz: status = pending, createdAt, Hash des Request-Payloads.
- Senden Sie die Anfrage mit dem Schlüssel.
- Bei Erfolg setzen Sie status = done und speichern die Server-Resultat-ID.
- Bei einem Retry verwenden Sie denselben Schlüssel, nicht einen neuen.
Gerade das Wiederverwenden desselben Schlüssels verhindert versehentliche Doppelbuchungen.
App-Neustarts und Offline-Lücken behandeln
Wenn die App während einer Anfrage beendet wird, muss der nächste Start sicher sein. Speichern Sie den Idempotency-Key und den Request-Status lokal (z. B. in einer kleinen Datenbankzeile). Beim Neustart retryen Sie entweder mit demselben Schlüssel oder rufen eine "Status prüfen"-API mit dem gespeicherten Schlüssel oder der Server-Resultat-ID auf.
Serverseitig sollte der Vertrag klar sein: Bei Empfang eines doppelten Schlüssels soll der Server den zweiten Versuch ablehnen oder die Originalantwort (gleiche Bestell-ID, gleiche Quittung) zurückgeben. Kann der Server das noch nicht, wird clientseitige Duplikatsvermeidung nie vollständig zuverlässig sein, weil die App nicht sehen kann, was nach dem Senden passiert ist.
Eine nutzerfreundliche Ergänzung: Wenn ein Versuch pending ist, zeigen Sie „Zahlung läuft" und deaktivieren Sie den Button, bis ein finales Ergebnis vorliegt.
UI-Muster, die versehentliche Mehrfachsendungen reduzieren
Langsame Verbindungen zerstören nicht nur Requests — sie verändern, wie Menschen tippen. Wenn der Bildschirm zwei Sekunden einfriert, gehen viele Nutzer davon aus, nichts sei passiert, und tippen erneut. Ihre UI muss „einmal tippen" zuverlässig fühlbar machen, auch wenn das Netzwerk schlecht ist.
Optimistische UI ist sicher, wenn die Aktion reversibel oder risikoarm ist (ein Item favorisieren, Entwurf speichern, Nachricht als gelesen markieren). Bestätigte UI ist besser für Geld, Inventar, irreversible Löschungen und alles, was Duplikate verursachen könnte.
Ein guter Standard für kritische Aktionen ist ein klarer Pending-Zustand. Nach dem ersten Tap wechseln Sie sofort den Primärbutton in einen „Submitting..."-Zustand, deaktivieren ihn und zeigen eine kurze Erläuterung, was passiert.
Muster, die bei instabilen Netzen gut funktionieren:
- Deaktivieren Sie die Primäraktion nach dem Tippen und halten Sie sie deaktiviert, bis ein finales Ergebnis vorliegt.
- Zeigen Sie einen sichtbaren "Pending"-Status mit Details (Betrag, Empfänger, Artikelanzahl).
- Fügen Sie eine einfache Ansicht „Letzte Aktivitäten" hinzu, damit Nutzer bestätigen können, was sie bereits gesendet haben.
- Wenn die App in den Hintergrund geht, behalten Sie den Pending-Status bei, wenn der Nutzer zurückkehrt.
- Bevorzugen Sie einen klaren primären Button statt mehrerer Tippziele auf demselben Bildschirm.
Manchmal gelingt die Anfrage, die Antwort geht aber verloren. Behandeln Sie das als normales Ergebnis, nicht als Fehler, der zu wiederholtem Tippen einlädt. Statt „Fehlgeschlagen, erneut versuchen" zeigen Sie „Wir sind uns noch nicht sicher" und bieten eine sichere Option wie „Status prüfen" an. Wenn Sie den Status nicht prüfen können, behalten Sie die lokale Pending-Aufzeichnung und informieren Sie den Nutzer, sobald die Verbindung wiederhergestellt ist.
Machen Sie „Erneut versuchen" explizit und sicher. Zeigen Sie es nur, wenn Sie die Anfrage mit derselben Client-Request-ID oder demselben Idempotency-Key wiederholen können.
Realistisches Beispiel: ein instabiler Checkout-Versuch
Ein Kunde ist im Zug mit lückenhaftem Empfang. Er legt Artikel in den Warenkorb und tippt auf Bezahlen. Die App muss geduldig sein, darf aber auch keine zwei Bestellungen erzeugen.
Eine sichere Abfolge sieht so aus:
- Die App erzeugt eine clientseitige Attempt-ID und sendet die Checkout-Anfrage mit einem Idempotency-Key (z. B. einer UUID, die mit dem Warenkorb gespeichert wird).
- Die Anfrage wartet bis zum Connect-Timeout und dann länger bis zum Read-Timeout. Der Zug fährt in einen Tunnel und der Call läuft in Timeout.
- Die App versucht es einmal erneut, aber nur nach kurzer Verzögerung und nur, wenn nie eine Server-Antwort empfangen wurde.
- Der Server empfängt die zweite Anfrage und sieht denselben Idempotency-Key, also liefert er das Originalergebnis zurück, anstatt eine neue Bestellung anzulegen.
- Die App zeigt bei Erhalt der Erfolgsantwort eine Abschlussseite, auch wenn die Antwort vom Retry stammt.
Caching folgt strengen Regeln. Produktlisten, Lieferoptionen und Steuertabellen können kurzfristig gecacht werden (GET). Die Checkout-Abschickung (POST) wird niemals gecacht. Selbst mit HTTP-Cache behandeln Sie ihn als Lesehilfe fürs Browsen, nicht als Speicher für Zahlungen.
Duplikatsvermeidung ist eine Kombination aus Netzwerk- und UI-Entscheidungen. Wenn der Nutzer auf Bezahlen tippt, wird der Button deaktiviert und der Bildschirm zeigt „Bestellung wird abgeschickt..." mit einer einzigen Abbrechen-Option. Verliert die App das Netz, wechselt sie zu „Wir versuchen es weiter" und behält dieselbe Attempt-ID. Schließt der Nutzer die App und öffnet sie neu, kann die App mithilfe dieser ID den Bestellstatus prüfen, anstatt erneut zur Zahlung aufzufordern.
Kurze Checkliste und nächste Schritte
Wenn Ihre App auf Büro-WLAN „ziemlich gut" läuft, aber in Zügen, Aufzügen oder ländlichen Gebieten auseinanderfällt, behandeln Sie das als Release-Gate. Diese Arbeit dreht sich weniger um cleveren Code und mehr um klare, wiederholbare Regeln.
Checkliste vor dem Release:
- Setzen Sie Timeouts pro Endpunkt-Typ (Login, Feed, Upload, Checkout) und testen Sie mit gedrosselten und hochlatenzigen Netzen.
- Retryen Sie nur dort, wo es wirklich sicher ist, und begrenzen Sie die Retries mit Backoff (ein paar Versuche für Lesevorgänge, meist keine für Schreibvorgänge).
- Fügen Sie für jeden kritischen Schreibvorgang einen Idempotency-Key hinzu (Zahlungen, Bestellungen, Formular-Einreichungen), damit ein Retry oder Doppeltippen keine Duplikate erzeugt.
- Machen Sie Caching-Regeln explizit: was veraltet sein darf, was frisch sein muss und was niemals gecacht werden darf.
- Machen Sie Zustände sichtbar: pending, failed und completed sollten unterschiedlich aussehen, und die App sollte abgeschlossene Aktionen nach einem Neustart wiedererkennen.
Wenn eine dieser Punkte mit „entscheidet sich später" beantwortet wird, bekommen Sie über verschiedene Bildschirme verteilt zufälliges Verhalten.
Nächste Schritte, damit es Bestand hat
Schreiben Sie eine einseitige Netzwerk-Policy: Endpunktkategorien, Timeout-Ziele, Retry-Regeln und Caching-Erwartungen. Erzwingen Sie sie an einer Stelle (Interceptor, gemeinsame Client-Factory oder kleine Wrapper), sodass jedes Teammitglied standardmäßig dasselbe Verhalten erhält.
Machen Sie dann eine kurze Duplikat-Übung. Wählen Sie eine kritische Aktion (z. B. Checkout), simulieren Sie einen eingefrorenen Spinner, schließen Sie die App zwangsweise, schalten Sie den Flugmodus um und drücken Sie erneut den Button. Wenn Sie nicht beweisen können, dass es sicher ist, werden Nutzer es früher oder später kaputtmachen.
Wenn Sie dieselben Regeln auf Backend und Clients implementieren möchten, ohne alles per Hand zu verdrahten, kann AppMaster (appmaster.io) helfen, Produktions-Backends und native Mobile-Quellcodes zu generieren. Selbst dann ist die Policy der Schlüssel: definieren Sie Idempotenz, Retries, Caching und UI-Zustände einmal und wenden Sie sie konsistent entlang des gesamten Flows an.
FAQ
Beginnen Sie damit, für jeden Bildschirm und jede Aktion klar zu definieren, was „korrekt" bedeutet — besonders für Dinge, die höchstens einmal passieren dürfen, wie Zahlungen oder Bestellungen. Wenn die Regeln klar sind, stimmen Sie Timeouts, Retries, Caching und UI-Zustände darauf ab, anstatt sich auf Bibliotheksstandards zu verlassen.
Nutzer sehen meist endlose Ladezeichen, Fehler nach langem Warten, Aktionen, die beim zweiten Versuch funktionieren, oder doppelte Ergebnisse wie zwei Bestellungen oder doppelte Abbuchungen. Das kommt oft von unklaren Retry- und "pending vs failed"-Regeln, nicht nur von schlechtem Empfang.
Verwenden Sie den Connect-Timeout für die Zeit zum Aufbau der Verbindung, Write-Timeout für das Senden des Request-Bodys (Uploads) und Read-Timeout für das Warten auf die Antwort. Als Faustregel: kürzere Timeouts für unkritische Lesevorgänge, längere Read/Write-Timeouts für kritische Vorgänge, und eine sichtbare UI-Grenze, damit Nutzer nicht endlos warten.
Ja — wenn Sie nur eines setzen, nutzen Sie callTimeout, um den gesamten Vorgang zu begrenzen und ein "unendliches" Warten zu vermeiden. Zusätzlich können Sie connect/read/write-Timeouts setzen, um Uploads und langsame Antwortkörper feiner zu steuern.
Retryen Sie zunächst nur vorübergehende Fehler wie Verbindungsabbrüche, DNS-Probleme und Timeouts sowie gelegentlich 502/503/504. Vermeiden Sie Retries für 4xx-Fehler und automatische Retries für Schreibvorgänge, es sei denn, Sie haben Idempotenz-Schutz, denn sonst können Duplikate entstehen.
Nutzen Sie wenige Versuche (häufig 1–3) mit exponentiellem Backoff und etwas Zufallsjitter, damit viele Geräte nicht gleichzeitig erneut anfragen. Begrenzen Sie außerdem die Gesamtdauer der Retries, damit der Nutzer schnell ein klares Ergebnis statt eines minutenlangen Spinners sieht.
Idempotenz bedeutet, dass das wiederholte Senden derselben Anfrage kein zweites Ergebnis erzeugt. Das verhindert doppelte Abbuchungen oder doppelte Bestellungen. Bei kritischen Aktionen senden Sie pro Versuch einen Idempotency-Key und verwenden denselben Schlüssel bei Wiederholungen, damit der Server die ursprüngliche Antwort zurückgeben kann.
Erzeugen Sie den eindeutigen Schlüssel, wenn der Nutzer die Aktion startet, speichern Sie ihn lokal mit einem kleinen "pending"-Datensatz und senden Sie ihn mit der Anfrage. Bei Retry oder App-Neustart verwenden Sie denselben Schlüssel erneut oder prüfen den Status, damit eine Nutzerintention nicht in zwei Server-Schreibvorgänge verwandelt wird.
Cachen Sie nur Daten, die etwas älter sein dürfen, und erzwingen Sie frische Prüfungen bei Geld-, Sicherheits- oder Abschlussentscheidungen. Für Leseendpunkte sind kurze Frischhaltezeiten mit Revalidierung und ETags empfehlenswert; für Schreibvorgänge überhaupt kein Cache, und sensible Antworten mit no-store behandeln.
Deaktivieren Sie die primäre Schaltfläche nach dem ersten Tap, zeigen Sie sofort einen „Submitting…"-Zustand und behalten Sie einen sichtbaren Pending-Status bei, der Backgrounding oder Neustarts überdauert. Wenn die Antwort verloren gehen könnte, zeigen Sie keine Aufforderung zum wiederholten Tippen, sondern eine unsichere Meldung („Wir sind uns noch nicht sicher") mit einer sicheren Option wie „Status prüfen".


