Table of Contents

Cursors & Pagination

If you've looked at the source code for the Notifications or Timeline samples you may have noticed they page through notifications rather than get all the notifications at once.

Each sample uses the limit and cursor parameters to get their results one page at a time.

For example, to page through notifications, with each page containing a maximum of five results you would write the following:

AtProtoHttpResult<NotificationsView> notifications = 
     await agent.ListNotifications(5);

The first call to ListNotifications() uses the limit parameter to control how many notifications are returned from the API.

If you don't pass a limit Bluesky uses a default page size limit, which can vary by API.

Check each response and follow its cursor until there are no more pages. Since cursors are supplied by the API, also track cursors already used and impose a page limit:

const int maximumPages = 100;
int pagesRead = 1;
HashSet<string> seenCursors = new(StringComparer.Ordinal);

notifications.EnsureSucceeded();

while (true)
{
    // Do whatever needs to be done on the page
    // of notifications.

    string? nextCursor = notifications.Result.Cursor;

    if (string.IsNullOrEmpty(nextCursor))
    {
        break;
    }

    if (!seenCursors.Add(nextCursor))
    {
        throw new InvalidOperationException("The API returned a cursor that was already used.");
    }

    if (pagesRead >= maximumPages)
    {
        throw new InvalidOperationException($"Pagination exceeded the limit of {maximumPages} pages.");
    }

    pagesRead++;
    notifications = await agent.ListNotifications(
        limit: 5,
        cursor: nextCursor);
    notifications.EnsureSucceeded();
}

You can see that there's a difference between the first call to ListNotifications() and the second, the addition of the cursor parameter. This parameter is how Bluesky APIs implement paging.

The response from the ListNotifications() API, or any API that supports paging, includes a cursor every time more things are available. Passing the cursor from a previous API response to a new request gets the next page of data. The AT Proto documentation has a section on Cursors and Pagination.

APIs that support pagination include ListNotifications(), SearchActors(), GetTimeline(), GetFeed() and GetSuggestions().

Tip

Cursors aren't a standard format - a feed can generate whatever they type of cursor they want, and use that to decide what to serve next. The Feed sample pages through the Bluesky Discovery feed. This feed uses the cursor to track what it's already shown you, so as you load more and more pages the cursor grows and grows, until, if you page for long enough, the cursor is too big to send in the request and you get a 400 Bad Request response. This is why the feed sample only loads 10 pages of 5 posts.

Warning

Cursors are supplied by the API and are opaque; do not assume they always increase or that a repeated cursor will be returned immediately. A faulty or untrusted API can return the same cursor repeatedly, cycle between multiple cursors, or reset an earlier cursor sequence after some number of responses. A pagination loop can then repeat pages or make requests indefinitely. Applications that paginate should keep a set of cursors already used and stop if a cursor repeats, and also impose a maximum page/request count or deadline because a server can keep returning new cursors forever. Cursor-cycle checks help detect looping; they do not prove that pages are complete, unique, or trustworthy.