KoderKoder.ai
PreiseEnterpriseBildungFür Investoren
AnmeldenLoslegen

Produkt

PreiseEnterpriseFür Investoren

Ressourcen

Kontakt aufnehmenSupportBildungBlog

Rechtliches

DatenschutzrichtlinieNutzungsbedingungenSicherheitRichtlinie zur akzeptablen NutzungMissbrauch melden

Soziales

LinkedInTwitter
Koder.ai
Sprache

© 2026 Koder.ai. Alle Rechte vorbehalten.

Startseite›Blog›Cursor‑Paginierung für stabile API‑Listen ohne mysteriöse Fehler
18. Dez. 2025·5 Min

Cursor‑Paginierung für stabile API‑Listen ohne mysteriöse Fehler

Cursor‑Paginierung sorgt dafür, dass Listen stabil bleiben, wenn sich Daten ändern. Erfahre, warum Offset‑Paging bei Einfügungen und Löschungen versagt und wie man saubere Cursors implementiert.

Cursor‑Paginierung für stabile API‑Listen ohne mysteriöse Fehler

Das Problem: Seiten, die sich unter deinen Fingern verändern

Du öffnest einen Feed, scrollst ein bisschen — und plötzlich läuft etwas schief. Du siehst denselben Eintrag zweimal. Etwas, das gerade noch da war, fehlt. Eine Zeile, die du antippen wolltest, rutscht weg und du landest auf der falschen Detailseite.

Das sind für Nutzer sichtbare Fehler, auch wenn deine API-Antworten einzeln betrachtet „korrekt“ aussehen. Die üblichen Symptome sind leicht zu erkennen:

  • Doppelte Einträge über Seiten hinweg
  • Fehlende Einträge, die nie angezeigt wurden
  • Einträge, die beim Scrollen ihre Position ändern
  • Unendliches Scrollen, das zu früh stoppt oder dieselbe Seite erneut lädt

Auf Mobilgeräten wird das noch schlimmer. Leute pausieren, wechseln die App, verlieren die Verbindung und machen später weiter. In der Zwischenzeit kommen neue Einträge dazu, alte werden gelöscht und manche bearbeitet. Wenn deine App weiterhin „Seite 3“ per Offset anfragt, können Seiten­grenzen während des Scrollens verschoben werden. Das Ergebnis ist ein Feed, der sich instabil und unzuverlässig anfühlt.

Das Ziel ist einfach: Sobald ein Nutzer vorwärts scrollt, sollte die Liste wie eine Momentaufnahme funktionieren. Neue Elemente können existieren, dürfen aber nicht das reshufflen verursachen, was der Nutzer bereits durchblättert. Der Nutzer sollte eine gleichmäßige, vorhersagbare Folge erhalten.

Keine Paginierungsmethode ist perfekt. In echten Systemen gibt es gleichzeitige Schreibvorgänge, Bearbeitungen und mehrere Sortieroptionen. Aber Cursor-Paginierung ist meist sicherer als Offset-Paginierung, weil sie von einer festen Position in einer stabilen Reihenfolge aus paginiert, statt von einer sich bewegenden Zeilenzahl.

Offset-Paginierung in einer Minute

Offset-Paginierung ist das „überspringe N, nimm M“-Verfahren. Du sagst der API, wie viele Einträge sie überspringen soll (offset) und wie viele sie zurückgeben soll (limit). Mit limit=20 bekommst du 20 Elemente pro Seite.

Konzepte:

  • GET /items?limit=20&offset=0 (erste Seite)
  • GET /items?limit=20&offset=20 (zweite Seite)
  • GET /items?limit=20&offset=40 (dritte Seite)

Die Antwort enthält normalerweise die Elemente plus genug Infos, um die nächste Seite anzufordern.

{
  "items": [
    {"id": 101, "title": "..."},
    {"id": 100, "title": "..."}
  ],
  "limit": 20,
  "offset": 20,
  "total": 523
}

Es ist beliebt, weil es sich gut auf Tabellen, Admin‑Listen, Suchergebnisse und einfache Feeds abbilden lässt. Mit SQL ist es leicht umzusetzen via LIMIT und OFFSET.

Der Haken ist die versteckte Annahme: das Dataset bleibt still, während der Nutzer durchblättert. In echten Apps verschieben sich Reihen — neue werden eingefügt, alte gelöscht, Sortierschlüssel ändern sich. Hier beginnen die „mysteriösen Fehler“.

Warum Offset bei Einfügungen oder Löschungen versagt

Offset-Paginierung geht davon aus, dass die Liste zwischen den Anfragen gleich bleibt. Aber echte Listen bewegen sich. Wenn sich die Liste verschiebt, zeigt ein Offset wie „überspringe 20“ nicht mehr auf die gleichen Elemente.

Stell dir einen Feed vor, sortiert nach created_at desc (neuste zuerst), Seiten­größe 3.

Du lädst Seite 1 mit offset=0, limit=3 und bekommst [A, B, C].

Jetzt wird ein neues Element X erstellt und erscheint ganz oben. Die Liste ist nun [X, A, B, C, D, E, F, ...]. Du lädst Seite 2 mit offset=3, limit=3. Der Server überspringt [X, A, B] und gibt [C, D, E] zurück.

Du hast C gerade wieder gesehen (ein Duplikat), und später wirst du ein Element verpassen, weil sich alles nach unten verschoben hat.

Löschungen verursachen das Gegenproblem. Beginne mit [A, B, C, D, E, F, ...]. Du lädst Seite 1 und siehst [A, B, C]. Bevor du Seite 2 lädst, wird B gelöscht, also ist die Liste [A, C, D, E, F, ...]. Seite 2 mit offset=3 überspringt [A, C, D] und gibt [E, F, G] zurück. D wird zu einer Lücke, die du nie abrufst.

In Feeds, die neueste zuerst zeigen, passieren Einfügungen oben — genau das verschiebt jedes spätere Offset.

Was eine stabile Liste für Web und Mobile bedeutet

Eine „stabile Liste“ ist das, was Nutzer erwarten: beim Vorwärts­scrollen springen Elemente nicht, wiederholen sich nicht oder verschwinden nicht ohne ersichtlichen Grund. Es geht weniger darum, die Zeit anzuhalten, als die Paginierung vorhersagbar zu machen.

Zwei Ideen werden oft vermischt:

  • Stabile Sortierung: es gibt eine klare Sortierregel (z. B. created_at mit einem Tie‑Breaker wie id), sodass zwei Anfragen mit denselben Eingaben die gleiche Reihenfolge zurückgeben.
  • Stabile Paginierung: sobald ein Nutzer vorwärts scrollt, setzt die nächste Seite dort fort, wo das letzte Element war, selbst wenn neue Elemente hinzugefügt oder einige gelöscht werden.

Refresh und Scroll‑Forward sind unterschiedliche Aktionen. Refresh bedeutet „zeige mir, was jetzt neu ist“ — oben kann sich also ändern. Scroll‑Forward bedeutet „mach weiter von dort, wo ich war“ — du solltest keine Duplikate oder unerwartete Lücken sehen, die durch verschobene Seiten­grenzen entstehen.

Eine einfache Regel, die die meisten Paginierungsfehler verhindert: "Beim Vorwärts­scrollen dürfen niemals Wiederholungen angezeigt werden."

Cursor‑Paginierung: die einfache Idee

Cursor‑Paginierung bewegt sich durch eine Liste mit einem Lesezeichen statt mit einer Seitennummer. Statt „gib mir Seite 3“ sagt der Client „mach hier weiter“.

Der Vertrag ist klar:

  • Die API liefert ein Batch von Elementen plus einen Cursor, der die Position nach dem letzten Element repräsentiert.
  • Der Client sendet diesen Cursor zurück, um das nächste Batch zu erhalten.

Das verträgt Einfügungen und Löschungen besser, weil der Cursor an einer Position in der sortierten Reihenfolge anhaftet, nicht an einer Zeilenzahl.

Die nicht verhandelbare Voraussetzung ist eine deterministische Sortierreihenfolge. Du brauchst eine stabile Ordnung und einen konsistenten Tie‑Breaker, sonst ist der Cursor kein zuverlässiges Lesezeichen.

Wahl des Cursors und der Sortierung

Earn credits while building
Share what you learn about Koder.ai and earn credits through the content program.
Credits verdienen

Wähle zuerst die Sortierreihenfolge, die zur Art passt, wie Leute die Liste lesen. Feeds, Nachrichten und Aktivitätslogs sind meist „neueste zuerst“. Historien wie Rechnungen oder Audit‑Logs sind oft „älteste zuerst".

Ein Cursor muss eine Position in dieser Reihenfolge eindeutig identifizieren. Wenn zwei Elemente denselben Cursorwert teilen können, bekommst du irgendwann Duplikate oder Lücken.

Gängige Optionen und worauf du achten solltest:

  • created_at allein: einfach, aber unsicher, wenn viele Reihen denselben Timestamp haben.
  • id allein: sicher, wenn IDs monoton sind, passt aber vielleicht nicht zur gewünschten Produktreihenfolge.
  • created_at + id: meist die beste Kombination (Timestamp für die Produktreihenfolge, id als Tie‑Breaker).
  • updated_at als primäre Sortierung: riskant für unendliches Scrollen, weil Bearbeitungen Elemente zwischen Seiten verschieben können.

Wenn du mehrere Sortieroptionen anbietest, behandle jeden Sortiermodus als eigene Liste mit eigenen Cursor‑Regeln. Ein Cursor macht nur für genau eine Ordnung Sinn.

Schritt für Schritt: ein sauberes Cursor‑API‑Schema

Du kannst die API‑Oberfläche klein halten: zwei Eingaben, zwei Ausgaben.

1) Anfrage: limit + cursor

Sende ein limit (wie viele Items du möchtest) und optional einen cursor (wo du weitermachen willst). Fehlt der Cursor, liefert der Server die erste Seite.

Beispielanfrage:

GET /api/messages?limit=30&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0xNlQxMDowMDowMFoiLCJpZCI6Ijk4NzYifQ==

2) Antwort: items + next_cursor

Gib die Items und einen next_cursor zurück. Wenn es keine nächste Seite gibt, next_cursor: null. Clients sollten den Cursor als Token behandeln, nicht als etwas zum Bearbeiten.

{
  "items": [ {"id":"9876","created_at":"2026-01-16T10:00:00Z","subject":"..."} ],
  "next_cursor": "...",
  "has_more": true
}

Serverseitige Logik in einfachen Worten: in einer stabilen Reihenfolge sortieren, mit dem Cursor filtern, dann das Limit anwenden.

Wenn du nach neuestem zuerst (created_at DESC, id DESC) sortierst, dekodierst du den Cursor in (created_at, id), dann holst du Zeilen, bei denen (created_at, id) strikt kleiner ist als das Cursor‑Paar, wendest dieselbe Reihenfolge an und nimmst limit Zeilen.

3) Cursor‑Kodierung: opaques Token gewinnt

Du kannst den Cursor als base64‑JSON‑Blob kodieren (einfach) oder als signiertes/verschlüsseltes Token (mehr Arbeit). Opaque Tokens sind sicherer, weil du intern später ändern kannst, ohne Clients zu brechen.

Setze außerdem vernünftige Defaults: ein mobiles Default (oft 20–30), ein Web‑Default (oft 50) und ein hartes Server‑Max, damit ein fehlerhafter Client nicht 10.000 Zeilen anfordert.

Umgang mit Einfügungen, Löschungen und Bearbeitungen

Design refresh the right way
Prototype a clear refresh flow that shows new items without reshuffling scrolled pages.
Ausprobieren

Eine stabile Ansicht verspricht vor allem eines: sobald der Nutzer vorwärts scrollt, sollten die Elemente, die er noch nicht gesehen hat, nicht herumhüpfen, weil jemand anderes datensätze erstellt, löscht oder bearbeitet.

Mit Cursor‑Paginierung sind Einfügungen am einfachsten. Neue Datensätze sollten bei einem Refresh auftauchen, nicht in der Mitte bereits geladener Seiten. Wenn du nach created_at DESC, id DESC sortierst, leben neue Elemente natürlich vor der ersten Seite, sodass dein existierender Cursor in ältere Elemente weiterläuft.

Löschungen sollten die Liste nicht neu anordnen. Wenn ein Element gelöscht wird, wird es einfach nicht mehr zurückgegeben, wenn du dort ankommen würdest. Wenn du gleichbleibende Seitengrößen brauchst, hole weiter, bis du limit sichtbare Elemente gesammelt hast.

Bearbeitungen sind der Punkt, an dem Teams leicht wieder Fehler einführen. Die Kernfrage ist: kann eine Bearbeitung die Sortierposition ändern?

Wähle Snapshot‑Verhalten oder Live‑Verhalten

Snapshot‑Verhalten ist meist besser für Scroll‑Listen: paginiere nach einem unveränderlichen Schlüssel wie created_at. Bearbeitungen können den Inhalt ändern, aber das Element springt nicht an eine neue Position.

Live‑Feed‑Verhalten sortiert etwa nach edited_at. Das kann Sprünge verursachen (ein altes Item wird bearbeitet und rückt nach oben). Wenn du das wählst, behandle die Liste als ständig veränderlich und gestalte die UX rund um Refresh.

Wenn das Cursor‑Element nicht mehr existiert

Mach den Cursor nicht davon abhängig, „diese exakte Zeile zu finden“. Kodier stattdessen die Position, z. B. {created_at, id} des zuletzt zurückgegebenen Elements. Dann basiert die nächste Abfrage auf Werten, nicht auf Zeilenexistenz:

  • Für absteigende Reihenfolge: WHERE (created_at, id) < (:created_at, :id)
  • Immer einen Tie‑Breaker (id) einschließen, um Duplikate zu vermeiden
  • Wenn das letzte Element gelöscht wurde, funktionieren die Werte trotzdem
  • Wenn das letzte Element bearbeitet wurde, funktioniert es weiterhin, solange deine Sortierschlüssel unveränderlich sind

Zurückblättern, Refresh und Sprünge

Vorwärts‑Paginierung ist der einfache Teil. Schwieriger sind Zurück‑Paging, Refresh und zufälliger Zugriff.

Für Zurück‑Paging funktionieren zwei Ansätze oft:

  • Gib beide Richtungen zurück (next_cursor für ältere Elemente und prev_cursor für neuere) und behalte eine on‑screen Sortierung bei.
  • Behalte einen einzigen Cursor, aber fordere bei nach oben scrollen eine umgekehrte Sortierung an.

Zufällige Sprünge sind mit Cursorn schwieriger, weil „Seite 20“ keine stabile Bedeutung hat, wenn sich die Liste ändert. Wenn du wirklich springen musst, springe zu einem Anker wie „um diesen Zeitstempel herum“ oder „beginnend ab dieser message id“, nicht zu einem Seitenindex.

Auf Mobilgeräten ist Caching wichtig. Speichere Cursors pro Listenstatus (Query + Filter + Sort) und behandle jeden Tab/View als eigene Liste. Das verhindert „Tab wechseln und alles gerät durcheinander“‑Verhalten.

Häufige Fehler, die mysteriöse Bugs erzeugen

Die meisten Cursor‑Paginierungsprobleme sind nicht die Datenbank. Sie entstehen durch kleine Inkonsistenzen zwischen Anfragen, die nur im realen Verkehr sichtbar werden.

Die größten Übeltäter:

  • Einen nicht‑eindeutigen Cursor verwenden (z. B. nur created_at), sodass Ties Duplikate oder fehlende Elemente erzeugen.
  • Einen next_cursor zurückgeben, der nicht mit dem tatsächlich zuletzt zurückgegebenen Element übereinstimmt.
  • Filter oder Sortierung zwischen Anfragen ändern.
  • Offset und Cursor im selben Endpoint mischen.

Wenn du Apps auf Plattformen wie Koder.ai baust, treten diese Edge‑Cases schnell auf, weil Web‑ und Mobile‑Clients denselben Endpoint teilen. Ein klarer Cursor‑Vertrag und eine deterministische Sortierregel halten beide Clients konsistent.

Schnell‑Checkliste, bevor du auslieferst

One API for all clients
Build shared pagination logic so your mobile and web lists behave the same.
Projekt erstellen

Bevor du Paginierung als „fertig“ deklarierst, prüfe das Verhalten bei Einfügungen, Löschungen und Retry‑Szenarien.

  • Sortierung ist explizit, deterministisch und beinhaltet einen Tie‑Breaker
  • Jede Anfrage wiederholt identische Filter und Sortierfelder
  • next_cursor stammt vom zuletzt zurückgegebenen Eintrag
  • limit hat ein sicheres Maximum und ein dokumentiertes Default
  • Refresh‑Verhalten ist definiert (wie neue Elemente erscheinen)

Für Refresh wähle eine klare Regel: entweder Nutzer ziehen zum Aktualisieren, um neuere Elemente oben zu sehen, oder du fragst periodisch „gibt es etwas neueres als mein erstes Element?“ und zeigst einen „Neue Elemente“-Button. Konsistenz sorgt dafür, dass die Liste stabil statt gespenstisch wirkt.

Ein realistisches Beispiel: ein Posteingang, der auf Geräten stabil bleibt

Stell dir ein Support‑Postfach vor, das Agenten im Web nutzen, während ein Manager dasselbe Postfach mobil prüft. Die Liste ist nach neusten zuerst sortiert. Erwartung: beim Vorwärts‑Scrollen springen Elemente nicht, wiederholen sich nicht oder verschwinden.

Mit Offset‑Paging lädt ein Agent Seite 1 (Items 1–20), dann Seite 2 (offset=20). Während er liest, kommen zwei neue Nachrichten oben an. Jetzt zeigt offset=20 auf eine andere Stelle als vorher. Der Nutzer sieht Duplikate oder verpasst Nachrichten.

Mit Cursor‑Paginierung fragt die App „die nächsten 20 Items nach diesem Cursor“ an, wobei der Cursor auf dem zuletzt tatsächlich gesehenen Item basiert (typischerweise (created_at, id)). Neue Nachrichten können jederzeit ankommen, aber die nächste Seite beginnt trotzdem direkt nach dem letzten Element, das der Nutzer gesehen hat.

Ein einfacher Test vor dem Rollout:

  • Beginne mit dem Abrufen von Seiten, während ein Skript jede Sekunde neue Nachrichten einfügt
  • Lösche ein paar Nachrichten mitten im Scroll
  • Bearbeite eine Nachricht (ohne die Sortierfelder zu ändern)
  • Bestätige, dass du niemals Wiederholungen, Lücken oder falsch geordnete Elemente bekommst
  • Verifiziere, dass die mobile und die Web‑App konsistente Seitenbegrenzungen zeigen

Wenn du schnell prototypst, kann Koder.ai dir helfen, den Endpoint und die Client‑Flows aus einem Chat‑Prompt zu skizzieren und dann sicher mit Planning Mode, Snapshots und Rollbacks zu iterieren, falls eine Paginierungsänderung beim Testen überrascht.

FAQ

Why do I get duplicates or missing items when I paginate with offset?

Offset-Paginierung sagt im Grunde „überspringe N Zeilen“. Wenn zwischen zwei Abfragen neue Zeilen eingefügt oder alte gelöscht werden, verschiebt sich die Zeilenzahl. Dasselbe Offset kann plötzlich auf andere Elemente zeigen als vorhin — das erzeugt für Nutzer Duplikate und Lücken beim Scrollen.

How does cursor pagination prevent the “list changed under me” problem?

Cursor-Paginierung verwendet ein Lesezeichen, das die Position „nach dem letzten gesehenen Element“ repräsentiert. Die nächste Anfrage setzt genau an dieser Position in einer deterministischen Reihenfolge fort, sodass Einfügungen oben und Löschungen in der Mitte die Seitenbegrenzung nicht so verschieben wie Offsets.

What should I use as a cursor: created_at, id, or both?

Verwende eine deterministische Sortierung mit einem Tie-Breaker, meist (created_at, id) in derselben Richtung. created_at gibt die produktfreundliche Reihenfolge, id macht jede Position eindeutig, sodass du bei gleichen Zeitstempeln nichts wiederholst oder überspringst.

Can I paginate by updated_at for a live feed?

Nach updated_at zu sortieren kann dazu führen, dass Elemente beim Bearbeiten zwischen Seiten springen — das bricht die Erwartung einer stabilen Vorwärts-Scroll-Erfahrung. Wenn du eine „zuletzt bearbeitet“-Ansicht brauchst, gestalte die UI für regelmäßige Aktualisierungen und akzeptiere Reordering statt einer stabilen unendlichen Scroll.

What should my API response include for cursor pagination?

Gib ein opaques Token als next_cursor zurück und lass den Client es unverändert zurücksenden. Eine einfache Methode ist, das letzte Element (created_at, id) als base64-kodiertes JSON zu übergeben. Wichtig ist, dass der Cursor als undurchsichtiges Token behandelt wird, damit du intern später ändern kannst, ohne Clients zu brechen.

What happens if the item referenced by the cursor gets deleted?

Baue die nächste Abfrage aus den Cursorwerten auf, nicht daraus „finde diese exakte Zeile“. Wenn das letzte Element gelöscht wurde, definiert das gespeicherte (created_at, id) trotzdem eine Position, sodass du sicher mit einer strikt-< (oder >) Bedingung im selben Sort fortfahren kannst.

How do I avoid repeats when fetching the next page?

Verwende einen strikten Vergleich und einen eindeutigen Tie-Breaker und nimm den Cursor immer vom tatsächlich zuletzt zurückgegebenen Element. Die meisten Wiederholungsfehler entstehen, weil <= statt < verwendet wird, der Tie-Breaker fehlt oder der next_cursor vom falschen Element generiert wurde.

How should refresh work with cursor pagination?

Triff eine klare Entscheidung: Refresh lädt neuere Elemente oben, während Scroll-Forward von deinem existierenden Cursor aus in ältere Elemente fortsetzt. Vermische nicht die Refresh-Semantik mit dem Cursor-Fluss, sonst sehen Nutzer Reordering und empfinden die Liste als unzuverlässig.

Can I reuse the same cursor if the user changes filters or sort order?

Ein Cursor gilt nur für eine genaue Sortierung und eine feste Filtermenge. Wenn der Client Sortiermodus, Suche oder Filter ändert, muss er eine neue Paginierungssitzung ohne Cursor beginnen und Cursors pro Listenstatus getrennt speichern.

How do I support jumping to a specific page or random access with cursors?

Cursor-Paginierung ist ideal für sequentielles Durchblättern, aber nicht für stabile „Seite 20“-Sprünge, weil sich das Dataset ändert. Wenn du springen musst, springe zu einem Anker wie „um diesen Zeitstempel herum“ oder „beginnend nach dieser id“ und paginiere von dort mit Cursorn.

Inhalt
Das Problem: Seiten, die sich unter deinen Fingern verändernOffset-Paginierung in einer MinuteWarum Offset bei Einfügungen oder Löschungen versagtWas eine stabile Liste für Web und Mobile bedeutetCursor‑Paginierung: die einfache IdeeWahl des Cursors und der SortierungSchritt für Schritt: ein sauberes Cursor‑API‑SchemaUmgang mit Einfügungen, Löschungen und BearbeitungenZurückblättern, Refresh und SprüngeHäufige Fehler, die mysteriöse Bugs erzeugenSchnell‑Checkliste, bevor du auslieferstEin realistisches Beispiel: ein Posteingang, der auf Geräten stabil bleibtFAQ
Teilen
Koder.ai
Erstellen Sie Ihre eigene App mit Koder heute!

Der beste Weg, die Leistungsfähigkeit von Koder zu verstehen, ist es selbst zu erleben.

Kostenlos startenDemo buchen