Cursor-Paginierung vs. Offset-Paginierung: Die optimale Wahl für Ihre API

Cursor-basierte Paginierung vs. Offset-Paginierung im Vergleich: Seitenverschiebung, Kosten für tiefe Offsets, Keyset-SQL, Stripe- und Slack-Beispiele und wie man beides in Apidog testet.

INEZA Felin-Michel

INEZA Felin-Michel

31 August 2026

Cursor-Paginierung vs. Offset-Paginierung: Die optimale Wahl für Ihre API

Apidog für Unternehmen

On-Premises Bereitstellung

SSO & RBAC

SOC 2 konform

Apidog Enterprise entdecken

Jeder Listen-Endpunkt steht irgendwann vor der gleichen Frage: Wie teilt man 2 Millionen Bestellungen in Seiten auf, die ein Client durchgehen kann? Wählt man Offset-Paginierung, erhält man einfaches SQL plus Seitenzahlen, die Benutzer verstehen. Wählt man Cursor-basierte Paginierung, erhält man stabile Ergebnisse und konsistente Latenz in jeder Tiefe, verzichtet aber auf die Möglichkeit, „zu Seite 47 zu springen“.

Die meisten Teams wählen Offset, weil es in jedem Tutorial die Standardeinstellung ist. Dann erreicht die Bestellungen-Tabelle ein paar Millionen Zeilen, Seite 4.000 beginnt, einen Timeout zu haben, und Benutzer berichten, denselben Datensatz beim Scrollen zweimal zu sehen. Dieser Leitfaden behandelt, wie beide Stile funktionieren, wo Offset an seine Grenzen stößt, warum Stripe und Slack Cursor verwenden und wie man beide Stile mit verketteten Anfragen in Apidog testen kann. Am Ende wissen Sie genau, welcher Stil zu Ihrem Endpunkt passt.

Wenn Sie zuerst das Gesamtbild wünschen, deckt unser API-Paginierungs-Leitfaden jede Strategie nebeneinander ab. Dieser Artikel geht tief auf die beiden wichtigsten ein.

Wie Offset-Paginierung funktioniert

Offset-Paginierung bildet direkt auf SQL ab. Der Client sendet eine Seitennummer und eine Seitengröße; der Server übersetzt diese in LIMIT und OFFSET.

SELECT id, customer_id, total_cents, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 25 OFFSET 50;

Diese Abfrage gibt Seite 3 Ihrer Bestellliste mit 25 Zeilen pro Seite zurück. Die Anfrage sieht so aus:

GET /v1/orders?page=3&per_page=25

Und eine typische Antwort:

{
  "data": [
    {
      "id": "ord_8821",
      "customer_id": "cus_1932",
      "total_cents": 4599,
      "created_at": "2026-08-30T14:22:07Z"
    }
  ],
  "page": 3,
  "per_page": 25,
  "total": 1848203,
  "total_pages": 73929
}

Der Reiz ist offensichtlich. Clients können zu jeder Seite springen. Der Server kann eine Gesamtzahl zurückgeben. Jeder Entwickler kann es an einem Nachmittag implementieren. Für eine kleine Administrationstabelle ist dies die richtige Wahl, und unser Schritt-für-Schritt-Leitfaden zur Paginierung in REST-APIs zeigt eine vollständige Offset-Implementierung.

Aber Offset birgt zwei strukturelle Probleme, und keines davon zeigt sich in der Entwicklung. Beide treten in der Produktion auf.

Problem 1: Seitenverschiebung (Page Drift)

Offset zählt Zeilen vom Anfang des sortierten Ergebnisses. Es weiß nichts darüber, welche Zeilen der Client bereits gesehen hat. Wenn also Zeilen zwischen Anfragen eingefügt oder gelöscht werden, verschieben sich die Seiten unter dem Client.

Angenommen, ein Benutzer lädt Seite 1 der Bestellungen, sortiert nach den neuesten zuerst, Zeilen 1 bis 25. Während des Lesens treffen 3 neue Bestellungen ein. Sie fordern Seite 2 an, die OFFSET 25 ist. Die Zeilen 23, 24 und 25 der ersten Antwort wurden nun auf die Positionen 26 bis 28 verschoben. Der Benutzer sieht sie erneut. Duplikate.

Löschen kehrt dies um. Entfernt man 3 Zeilen von Seite 1, während der Benutzer sie liest, überspringt OFFSET 25 nun 3 Zeilen, die der Benutzer nie gesehen hat. Stiller Datenverlust, und niemand erhält eine Fehlermeldung.

Für einen Monatsbericht, den niemand in Echtzeit scrollt, ist Drift harmlos. Für einen Aktivitäts-Feed, einen Synchronisations-Endpunkt oder alles, was ein Skript Seite für Seite durchläuft, während Schreibvorgänge fortgesetzt werden, bedeutet Drift duplizierte oder fehlende Datensätze. Verbraucher bemerken dies.

Problem 2: Tiefe Offsets scannen alles, was sie überspringen

OFFSET 500000 teleportiert nicht zu Zeile 500.001. Die Datenbank durchläuft den Index durch eine halbe Million Einträge, verwirft sie und gibt dann Ihre 25 Zeilen zurück. Die Kosten steigen linear mit der Tiefe: O(n), wobei n der Offset ist.

Konkrete Zahlen machen dies deutlich. Auf einer Postgres-Bestelltabelle mit 2 Millionen Zeilen und einem Index auf created_at:

Markus Winands Abhandlung über „no-offset“ auf Use The Index, Luke demonstriert diese Kosten mit Abfrageplänen und ist es wert, vollständig gelesen zu werden. Das Muster in der Produktion ist ein Slow-Query-Log, das von Anfragen mit hohem Offset dominiert wird, oft von einem Crawler, der pflichtbewusst jede Seite Ihrer öffentlichen API durchläuft. Ein einziger Client, und Ihr p99 verdoppelt sich.

Wie Cursor-basierte Paginierung funktioniert

Cursor-basierte Paginierung, auch als Keyset-Paginierung bezeichnet, verzichtet auf den Zeilenzähler. Anstatt „überspringe 50 Zeilen“ sagt der Client „gib mir Zeilen nach diesem spezifischen Datensatz“. Der Cursor identifiziert die letzte Zeile, die der Client gesehen hat, sodass der Server direkt zum nächsten Stapel springen kann.

Das SQL verwendet einen Zeilenvergleich auf dem Sortierschlüssel anstelle von OFFSET:

SELECT id, customer_id, total_cents, created_at
FROM orders
WHERE (created_at, id) < ('2026-08-30T14:22:07Z', 'ord_8821')
ORDER BY created_at DESC, id DESC
LIMIT 25;

Beachten Sie den Vergleich mit zwei Spalten. created_at allein ist nicht eindeutig; zwei Bestellungen können in derselben Millisekunde landen, und ein nicht-eindeutiger Sortierschlüssel bedeutet, dass Zeilen an Seitengrenzen übersprungen oder wiederholt werden. Das Hinzufügen von id als Tiebreaker macht die Sortierung vollständig und die Paginierung exakt. Mit einem zusammengesetzten Index auf (created_at, id) springt die Datenbank direkt zur Grenze und liest 25 Einträge. Seite 1 und Seite 60.000 kosten dasselbe.

Die API sollte diese Rohwerte jedoch nicht offenlegen. Echte Implementierungen kodieren den Sortierschlüssel in einem undurchsichtigen Token, normalerweise Base64:

GET /v1/orders?limit=25&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNDoyMjowN1oiLCJpZCI6Im9yZF84ODIxIn0

Opazität ist eine Designentscheidung, keine Verschleierung um ihrer selbst willen. Clients, die den Cursor nicht parsen können, können keine URLs manuell erstellen, was Ihnen die Freiheit gibt, den Sortierschlüssel zu ändern, einen Shard-Hinweis hinzuzufügen oder Speichermodule zu wechseln, ohne etwas zu beschädigen. Der Vertrag lautet dann „gib zurück, was wir dir gegeben haben“, nichts weiter.

Der Kompromiss: Es gibt keine Seite 47. Ein Cursor kennt nur „nach dieser Zeile“, sodass Clients Seite für Seite vorwärts (und rückwärts, wenn Sie einen vorherigen Cursor ausgeben) gehen. Gesamtzahlen sind ebenfalls nicht kostenlos; Zählen ist eine separate Abfrage. Für Designs, bei denen der Datensatz selbst riesig ist, behandelt unser Leitfaden zum Design von API-Paginierung für Millionen von Datensätzen die Skalierungsseite detaillierter.

Vor- und Nachteile auf einen Blick

Dimension Offset-Paginierung Cursor-basierte Paginierung
Zu beliebiger Seite springen Ja, jede Seitennummer Nein, nur sequenzielles Durchlaufen
Gesamtzahl / Seitenzahl Günstig zu inkludieren Separate Zählabfrage
Leistung bei tiefen Seiten O(n), verschlechtert sich mit der Tiefe O(1) pro Seite in jeder Tiefe
Stabilität bei Schreibvorgängen Drift: Duplikate und Lücken Stabil, an eine Zeile gebunden
Implementierungskosten Trivial Moderat: Kodierung, Tiebreakers, Indexdesign
Sortieranforderungen Jedes ORDER BY funktioniert Benötigt einen eindeutigen, indizierten Sortierschlüssel
Caching von Seiten-URLs Einfach, URLs sind vorhersehbar Schwieriger, Cursor variieren pro Durchlauf
Client-Komplexität Niedrig Niedrig, wenn das Format sauber ist

Eine Feinheit in dieser Tabelle verdient besondere Hervorhebung: Cursor-Paginierung erfordert eine deterministische Sortierung. Wenn Ihr Endpunkt Clients erlaubt, nach einer veränderbaren, nicht-eindeutigen Spalte wie status zu sortieren, wird die Keyset-Logik schnell mühsam. Offset toleriert schlampige Sortierung; Cursor bestrafen sie.

Welche sollten Sie wählen?

Passen Sie den Stil daran an, wie die Daten konsumiert werden.

Admin-Tabellen und Dashboards: Offset. Interne Tools mit ein paar tausend Zeilen, Benutzer, die auf Seitenzahlen klicken, und eine sichtbare „1.848 Ergebnisse“-Anzeige. Drift spielt keine Rolle, die Tiefe bleibt gering, und „Springe zu Seite“ ist eine echte Funktion. Offset punktet bei den Implementierungskosten.

Unendliche Scroll-Feeds: Cursor. Niemand springt zu Seite 47 eines Feeds. Benutzer laden immer nur „mehr“, Schreibvorgänge erfolgen ständig, und Duplikate sind sichtbar und peinlich. Dies ist der klassische Cursor-Fall.

Öffentliche APIs: Cursor. Sie kontrollieren Ihre Konsumenten nicht. Jemand wird eine Schleife schreiben, die jede Seite durchläuft, und mit Offset werden tiefe Seiten um 3 Uhr morgens zu Ihrem Problem. Cursor halten jede Seite günstig und ermöglichen es Ihnen, interne Mechanismen hinter dem undurchsichtigen Token weiterzuentwickeln. Unser Leitfaden zur REST-API-Paginierung behandelt die URL- und Header-Konventionen im Detail.

Exporte und Synchronisierungsjobs: Cursor. Ein Batch-Job, der alle 2 Millionen Bestellungen zieht, benötigt zwei Garantien: keine fehlenden Zeilen trotz gleichzeitiger Schreibvorgänge und gleichbleibende Kosten pro Seite. Offset bietet beides nicht. Ein Cursor bietet auch einen kostenlosen Wiederaufnahmepunkt, wenn der Job bei Zeile 1,4 Millionen stirbt.

Die ehrliche Faustregel: Offset für kleine, von Menschen durchsuchte, zählbare Schnittstellen; Cursor für alles Große, Live- oder Öffentliche.

Wie echte APIs damit umgehen

Stripe ist vollständig Cursor-basiert. Jeder Listen-Endpunkt akzeptiert starting_after (eine Objekt-ID) und limit, und die Antworten enthalten has_more. Um die nächste Seite der Abbuchungen abzurufen, übergeben Sie die ID der letzten Abbuchung, die Sie erhalten haben. Die Stripe-Paginierungsdokumentation zeigt das Muster; beachten Sie, dass es nirgendwo eine Gesamtzahl gibt, eine bewusste Auslassung angesichts ihres Schreibvolumens.

Die REST-API von GitHub stellt auf den meisten Endpunkten immer noch page und per_page zur Verfügung, mit Link-Headern, die auf die nächste und letzte Seite verweisen. Aber lesen Sie die GitHub-Paginierungsdokumentation genau: Sie weist Clients an, den Link-Header wortgetreu zu befolgen, anstatt Seiten-URLs zu konstruieren, und neuere Endpunkte sind auf Cursor umgestiegen, genau weil tiefe Offset-Durchläufe über riesige Repositories schmerzhaft sind.

Slack hat seine Web-API auf Cursor-Paginierung umgestellt und kennzeichnet diese nun als den Ansatz, den alle neuen Methoden verwenden. Methoden wie conversations.history geben response_metadata.next_cursor zurück, und eine leere Cursor-Zeichenkette bedeutet, dass das Ende erreicht wurde, wie in der Slack-Paginierungsdokumentation beschrieben.

Drei APIs mit hohem Datenverkehr, und die Entwicklungsrichtung ist eindeutig: hin zu Cursorn.

Gestaltung der Antwort-Hülle (Response Envelope)

Eine Cursor-API steht oder fällt mit ihrer Hülle. Halten Sie sie langweilig und vorhersehbar:

{
  "data": [
    {
      "id": "ord_8846",
      "customer_id": "cus_2201",
      "total_cents": 12900,
      "created_at": "2026-08-30T16:01:44Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wOC0zMFQxNjowMTo0NFoiLCJpZCI6Im9yZF84ODQ2In0"
}

Vier Regeln machen es solide:

Beide Stile in Apidog testen

Paginierungsfehler verstecken sich an Grenzen: der letzten Seite, der leeren Seite, dem Cursor, dessen Ankerzeile gelöscht wurde. Manuelles Klicken wird sie nicht aufdecken, aber ein verkettetes Testszenario wird es, und hier verdient Apidog seinen Platz im Workflow.

Für Cursor-Endpunkte erstellen Sie ein Testszenario mit zwei Schritten:

  1. Rufen Sie den Endpunkt auf und extrahieren Sie den Cursor. Fügen Sie der ersten Anfrage einen Post-Prozessor mit dem JSONPath $.next_cursor hinzu und speichern Sie ihn in einer Variable wie nextCursor. Apidog ermöglicht Ihnen, den JSONPath direkt aus dem Antwortpanel zu kopieren; die vollständige Anleitung finden Sie unter wie man Assertions setzt und Variablen mit JSONPath extrahiert.
  2. Wiederholen Sie die Anfrage für die nächste Seite. Verpacken Sie eine zweite Anfrage in einem ForEach- oder Loop-Schritt, übergeben Sie {{nextCursor}} als Cursor-Parameter, extrahieren Sie $.next_cursor bei jeder Iteration erneut und beenden Sie, wenn has_more falsch ist. Prüfen Sie bei jedem Durchlauf, dass keine id von der vorherigen Seite wiederholt wird und die Seitengröße niemals limit überschreitet.

Für Offset-Endpunkte gilt die gleiche Struktur mit einer Zählervariable: erhöhen Sie page, prüfen Sie, dass die Länge von data gleich per_page ist, bis zur letzten Seite, und prüfen Sie, dass total über den gesamten Durchlauf konsistent bleibt.

Fügen Sie dann die Grenzfälle als eigene Schritte hinzu, jeder mit expliziten Assertions:

Sobald das Szenario lokal erfolgreich ist, führen Sie es bei jedem Merge in CI aus. Laden Sie Apidog kostenlos herunter, und Sie können das vollständige Cursor-Durchlauf-Szenario, einschließlich Schleifen und Assertions, in weniger als einer halben Stunde ausführen.

FAQ

Ist Cursor-Paginierung immer besser?

Nein. Offset passt besser, wenn Benutzer Seitenzahlen, Gesamtzahlen und zufälligen Zugriff auf einen bescheidenen Datensatz benötigen, was die meisten internen Admin-Tools beschreibt. Cursor sind besser, wenn der Datensatz groß ist, Schreibvorgänge häufig sind oder die API öffentlich ist. Der Fehlerfall besteht darin, bei einem öffentlichen Listen-Endpunkt standardmäßig Offset zu verwenden und die O(n)-Kosten erst nach dem Start zu entdecken.

Wie erhalte ich eine Gesamtzahl mit Cursor-Paginierung?

Führen Sie eine separate SELECT COUNT(*) mit denselben Filtern aus, entweder als eigenständiger Endpunkt oder als optionaler Abfrageparameter wie include_count=true. Cachen Sie diese aggressiv; eine ungefähre, jede Minute aktualisierte Zählung genügt fast jeder Benutzeroberfläche. Stripe verzichtet vollständig auf Gesamtzahlen, was Ihnen sagt, wie oft Clients sie wirklich benötigen.

Kann ich beide Paginierungsstile auf einem Endpunkt anbieten?

Sie können dies tun, und GitHub tut dies effektiv während seiner Transition, aber vermeiden Sie es bei neuen APIs. Zwei Stile bedeuten zwei Sätze von Grenzbereichen, zwei Testmatrizen und Verwirrung beim Client, welchen er verwenden soll. Wählen Sie einen pro Endpunkt. Wenn Sie den Vertrag von Grund auf neu gestalten, sorgen die Muster in unserem Leitfaden zur REST-API-Paginierung dafür, dass die Parameternamen auf Ihrer gesamten Oberfläche konsistent bleiben.

Was passiert, wenn die Ankerzeile des Cursors gelöscht wird?

Bei der Keyset-Paginierung geht nichts kaputt. Der Vergleich WHERE (created_at, id) < (?, ?) erfordert nicht, dass die Ankerzeile existiert; er sucht die Grenzposition und fährt fort. Dies ist ein echter Vorteil gegenüber Designs, die „Cursor als Zeilensuche“ verwenden, und es ist genau der Grenzfall, den es in Ihrem Apidog-Testszenario zu überprüfen gilt, bevor ein Consumer ihn für Sie findet.

Praktizieren Sie API Design-First in Apidog

Entdecken Sie eine einfachere Möglichkeit, APIs zu erstellen und zu nutzen