Skip to main content

Music API Pagination: How to Handle Large Libraries, Batch Requests, and Cursor-Based Endpoints

Published on July 9, 2026

Music API Pagination: How to Handle Large Libraries, Batch Requests, and Cursor-Based Endpoints

A user's music library can hold 10,000+ tracks, hundreds of playlists, and years of listening history. Fetching all of that in a single API call is not just slow; most streaming services will reject the request outright. Pagination is how you retrieve large datasets in manageable chunks, and every music streaming API implements it differently.

This guide covers the pagination models you will encounter when building on music streaming APIs, the pitfalls that break naive implementations, and how to build production-grade pagination loops that work reliably across services.

Why Pagination Matters for Music APIs

Music libraries grow fast. A casual listener might have 500 saved tracks. A power user can easily exceed 15,000. Playlists, albums, and listening history multiply the data further. Streaming services enforce pagination to protect their infrastructure, keep response times predictable, and prevent individual API consumers from monopolizing bandwidth. If your app fetches a user's library, you will paginate. There is no shortcut.

Data TypeTypical Size (Power User)Default Page Size
Saved tracks5,000 - 20,00020 - 50
Playlists50 - 50020 - 50
Playlist tracks50 - 10,00050 - 100
Listening history1,000 - 50,00020 - 50
Search resultsVaries10 - 25

Ignoring pagination means your app either crashes on large libraries, silently drops data, or hits rate limits within seconds. All three outcomes frustrate users and generate support tickets.

Pagination Models Used by Streaming Services

Streaming platforms do not agree on a single pagination standard. You will encounter three main models, sometimes within the same service across different endpoints.

Offset-Based Pagination

The simplest model. You send an offset (how many items to skip) and a limit (how many to return). The API returns items starting at the offset position.

// Offset-based pagination example
const fetchTracks = async (offset = 0, limit = 50) => {
  const response = await fetch(
    `https://api.example.com/me/tracks?offset=${offset}&limit=${limit}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
  return response.json();
};

Offset pagination is easy to understand but breaks down at scale. If items are added or removed while you paginate, you will either skip items or receive duplicates. At high offsets (page 200+), database performance degrades because the service must count through all preceding rows.

Cursor-Based Pagination

Instead of a numeric offset, the API returns an opaque cursor string that points to the next batch of results. You pass the cursor back in your next request. The service resolves it server-side to the correct position.

// Cursor-based pagination example
const fetchWithCursor = async (cursor = null) => {
  const url = cursor
    ? `https://api.example.com/me/tracks?cursor=${cursor}&limit=50`
    : `https://api.example.com/me/tracks?limit=50`;
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` }
  });
  return response.json(); // { items: [...], next_cursor: "abc123" }
};

Cursor pagination handles concurrent modifications gracefully. Items added or removed between pages do not cause skips or duplicates. Performance stays constant regardless of how deep you are in the dataset.

Token-Based Pagination

A variant of cursor pagination where the API returns a nextPageToken or similar field. Functionally identical to cursors, but the naming convention and token format differ. Some services use short-lived tokens that expire after minutes, requiring you to restart pagination if you pause too long.

Pagination ModelStrengthsWeaknesses
Offset-basedSimple to implement, supports random accessSkips/duplicates on concurrent writes, slow at high offsets
Cursor-basedConsistent results, fast at any depthNo random page access, opaque cursor values
Token-basedSame as cursor-basedTokens may expire, same limitations as cursor

Common Pagination Pitfalls

Building a "just loop until empty" pagination function takes five minutes. Building one that works in production takes significantly more effort. Here are the traps that catch developers.

Stale Cursors and Expired Tokens

Some services invalidate cursors after a timeout (typically 5 to 15 minutes). If your app pauses between pages (user interaction, background processing, rate limit backoff), the cursor expires and the next request fails. Your code needs to detect this and restart from the beginning or the last known good position.

Rate Limit Interaction

Paginating a 10,000-track library at 50 items per page requires 200 API calls. Most streaming services enforce rate limits of 30 to 100 requests per minute per user. A naive loop without delays will hit rate limits by page 30 and receive 429 Too Many Requests responses. Your pagination loop must integrate with your rate limiting strategy.

Missing Items on Concurrent Writes

With offset-based pagination, if a user adds a track to position 1 while you are on page 5, every subsequent page shifts by one item. You either miss a track or receive a duplicate. There is no way to prevent this with offset pagination; you can only detect it by tracking total counts between pages.

Silent Truncation

Some services cap the maximum retrievable items regardless of pagination. For example, an endpoint might return a total of 15,000 but only allow you to retrieve the first 10,000 through pagination. Your code should detect when the API stops returning results before reaching the reported total.

// Naive pagination loop — breaks under real conditions
const getAllTracks = async () => {
  let allTracks = [];
  let offset = 0;
  const limit = 50;

  while (true) {
    const response = await fetchTracks(offset, limit);
    allTracks.push(...response.items);

    if (response.items.length < limit) break; // Assumes no gaps
    offset += limit; // No rate limit handling
    // No error handling
    // No stale cursor detection
    // No progress tracking
  }

  return allTracks; // May contain duplicates or missing items
};

Building a Reliable Pagination Loop

A production-grade pagination function handles retries, rate limits, progress reporting, and cursor expiration. Here is a pattern that covers these cases.

const paginateAll = async ({
  fetchPage,     // (cursor) => Promise<{ items, nextCursor, total }>
  onPage,        // (items, progress) => void
  maxRetries = 3,
  baseDelay = 1000,
}) => {
  let cursor = null;
  let allItems = [];
  let pageNumber = 0;

  while (true) {
    let attempts = 0;
    let page;

    while (attempts < maxRetries) {
      try {
        page = await fetchPage(cursor);
        break;
      } catch (error) {
        attempts++;

        if (error.status === 429) {
          const retryAfter = error.headers?.['retry-after'] || baseDelay * attempts;
          await sleep(retryAfter * 1000);
          continue;
        }

        if (error.message?.includes('expired_cursor')) {
          // Cursor expired — restart from beginning
          cursor = null;
          allItems = [];
          pageNumber = 0;
          continue;
        }

        if (attempts >= maxRetries) throw error;
        await sleep(baseDelay * Math.pow(2, attempts));
      }
    }

    allItems.push(...page.items);
    pageNumber++;

    if (onPage) {
      onPage(page.items, {
        pageNumber,
        totalSoFar: allItems.length,
        estimatedTotal: page.total,
      });
    }

    if (!page.nextCursor || page.items.length === 0) break;
    cursor = page.nextCursor;

    // Respectful delay between pages
    await sleep(200);
  }

  return allItems;
};

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

This function retries on rate limits with exponential backoff, restarts on expired cursors, reports progress per page, and adds a small delay between requests to stay under rate limits.

Handling rate limits and retry logic across multiple streaming services is tedious, error-prone work. Each service returns rate limit headers differently, enforces different windows, and uses different error codes. MusicAPI normalizes all of this behind a single interface, so your pagination code does not need per-service rate limit logic.

How MusicAPI Normalizes Pagination Across Services

When you build directly against each streaming service, you write a different pagination implementation for each one. Offset parameters, cursor formats, page size limits, and response shapes all vary. MusicAPI provides a unified API layer that normalizes pagination across all supported services.

Here is what fetching a user's playlists looks like with MusicAPI versus doing it yourself across multiple services:

With MusicAPI (one implementation for all services):

// Single pagination loop works for Spotify, Apple Music, Tidal, and more
const getUserPlaylists = async (connectionId) => {
  let allPlaylists = [];
  let cursor = null;

  do {
    const response = await fetch(
      `https://api.musicapi.com/user/playlists?` +
      `connectionId=${connectionId}&limit=50` +
      (cursor ? `&cursor=${cursor}` : ''),
      { headers: { Authorization: `Bearer ${MUSICAPI_KEY}` } }
    );
    const data = await response.json();
    allPlaylists.push(...data.items);
    cursor = data.nextCursor;
  } while (cursor);

  return allPlaylists;
};

Without MusicAPI (separate implementation per service):

// Service A: offset-based
const getPlaylistsServiceA = async () => {
  let offset = 0;
  // ... offset pagination logic, service-specific headers

  // Service B: cursor-based with expiring tokens
  // ... completely different pagination logic

  // Service C: page-number based with different auth
  // ... yet another pagination pattern
};

MusicAPI handles the translation layer: consistent cursor format across all services, unified page-size parameters, automatic retry on stale cursors, and normalized response shapes. You write one pagination loop, and it works for every supported service.

Check out endpoints like Get User Playlists and Get Favorite Tracks to see paginated responses in action.

FAQ

What is the maximum page size for music streaming API pagination?

Most streaming services cap page sizes between 50 and 100 items per request. Some endpoints allow up to 200. Requesting more than the maximum returns the service's default page size without an error. MusicAPI normalizes this with a consistent limit parameter across all services, capped at each service's actual maximum.

Can I paginate multiple endpoints in parallel?

Yes, but carefully. Parallel pagination across different endpoints (playlists and tracks simultaneously) works well because rate limits are typically per-user, not per-endpoint. Parallel pagination of the same endpoint risks cursor conflicts and can double your rate limit consumption. Use parallel requests for independent data types and sequential pagination within a single dataset.

How do I handle pagination when syncing a user's library in real-time?

Full library syncs should run as background jobs, not blocking operations. Paginate the entire library on first connection, store the results locally, then use webhook notifications or periodic delta syncs to catch changes. Avoid re-paginating the full library on every app launch; instead, track a sync timestamp and fetch only items modified after that point.

What happens when pagination fails mid-way through a large dataset?

Save your cursor or offset position after each successful page. If the process fails (network error, server timeout, app crash), resume from the last saved position instead of restarting from page one. For cursor-based APIs, check whether the saved cursor is still valid before resuming. If it has expired, restart the pagination but skip items you have already stored locally.

How can I improve pagination performance for large music libraries?

Three strategies work well together. First, request only the fields you need. Most APIs support field selection (fields=id,name,artist), which reduces payload size and response time. Second, use the largest page size the service allows to minimize the total number of requests. Third, implement connection pooling and keep-alive headers to reduce TCP handshake overhead across hundreds of sequential requests.

Does MusicAPI handle pagination automatically?

MusicAPI provides a consistent pagination interface across all supported services, with normalized cursors, unified page-size parameters, and built-in retry logic for expired cursors. You still control the pagination loop in your code (deciding when to fetch the next page, how to store results), but MusicAPI eliminates the per-service differences that make pagination code complex.


Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.