Support cursor-based pagination in GraphQL API #625

Closed
opened 2026-04-05 16:56:25 +02:00 by MrUnknownDE · 0 comments
Owner

Originally created by @jeremystretch on 1/9/2026

NetBox version

v4.5.0

Feature type

New functionality

Proposed functionality

The GraphQL API currently supporting paginating through results by passing offset and limit parameters with a request. For example:

query {
    device_list(
        pagination: {
            offset: $offset,
            limit: $limit
        }
    ) {
        id
        name
    }
}

This FR proposes an alternative to the offset parameter which specifies the primary key of the first object to fetch. For now, let's call this parameter start. Passing this parameter forces results to be ordered by primary key only, ensuring a deterministic set of results. The existing limit parameter can also be included to limit the number of results returned. For example:

query {
    device_list(
        pagination: {
            start: 4629,
            limit: 100
        }
    ) {
        id
        name
    }
}

The above query would yield (roughly) the following Django queryset:

Device.objects.order_by('pk').filter(pk__gte=4629)[:100]

The pagination strategy to be applied for a request is inferred from the parameter passed. The offset and start parameters are to be mutually exclusive.

Rough implementation plan

Subclass OffsetPaginationInput to introduce the start parameter:

import strawberry
from strawberry_django.pagination import OffsetPaginationInput

@strawberry.input
class StartIdPaginationInput(OffsetPaginationInput):
    start: int | None = None

Subclass StrawberryDjangoField to create a custom GraphQL field which employs our custom pagination by default:

from strawberry_django.fields.field import StrawberryDjangoField

class StartIdStrawberryDjangoField(StrawberryDjangoField):
    def apply_pagination(self, queryset, pagination=None):
        if pagination is not None:
            if start = getattr(pagination, "start", None):
                # TODO: Raise error if `offset` is also present
                queryset = queryset.filter(pk__gte=start).order_by("pk")

        return super().apply_pagination(queryset, pagination)

Use this field in place of strawberry_django.field() for all GraphQL lists:

# netbox/netbox/graphql/fields.py (new, or wherever NetBox keeps shared GraphQL helpers)
from functools import partial
import strawberry_django

from .pagination import StartIdPaginationInput, StartIdStrawberryDjangoField

netbox_field = partial(
    strawberry_django.field,
    pagination=StartIdPaginationInput,
    field_cls=StartIdStrawberryDjangoField,
)
from netbox.graphql.fields import netbox_field

foo_list: list[FooType] = netbox_field()

Use case

The current pagination approach relies on a relative offset. This struggles with very large result sets, because the offset must be calculated by scanning the entire table up to the offset position. Employing an object's unique primary key as a cursor avoids this, as we can leverage simple numeric comparison to filter for all PKs greater than or equal to a given value. Combined with the existing limit capability, this affords very efficient pagination at the cost of a fixed (and not very meaningful) ordering.

Database changes

N/A

External dependencies

N/A

*Originally created by @jeremystretch on 1/9/2026* ### NetBox version v4.5.0 ### Feature type New functionality ### Proposed functionality The GraphQL API currently supporting paginating through results by passing `offset` and `limit` parameters with a request. For example: ```graphql query { device_list( pagination: { offset: $offset, limit: $limit } ) { id name } } ``` This FR proposes an alternative to the `offset` parameter which specifies the primary key of the first object to fetch. For now, let's call this parameter `start`. Passing this parameter forces results to be ordered by primary key only, ensuring a deterministic set of results. The existing `limit` parameter can also be included to limit the number of results returned. For example: ```graphql query { device_list( pagination: { start: 4629, limit: 100 } ) { id name } } ``` The above query would yield (roughly) the following Django queryset: ```python Device.objects.order_by('pk').filter(pk__gte=4629)[:100] ``` The pagination strategy to be applied for a request is inferred from the parameter passed. The `offset` and `start` parameters are to be mutually exclusive. #### Rough implementation plan Subclass `OffsetPaginationInput` to introduce the `start` parameter: ```python import strawberry from strawberry_django.pagination import OffsetPaginationInput @strawberry.input class StartIdPaginationInput(OffsetPaginationInput): start: int | None = None ``` Subclass `StrawberryDjangoField` to create a custom GraphQL field which employs our custom pagination by default: ```python from strawberry_django.fields.field import StrawberryDjangoField class StartIdStrawberryDjangoField(StrawberryDjangoField): def apply_pagination(self, queryset, pagination=None): if pagination is not None: if start = getattr(pagination, "start", None): # TODO: Raise error if `offset` is also present queryset = queryset.filter(pk__gte=start).order_by("pk") return super().apply_pagination(queryset, pagination) ``` Use this field in place of `strawberry_django.field()` for all GraphQL lists: ```python # netbox/netbox/graphql/fields.py (new, or wherever NetBox keeps shared GraphQL helpers) from functools import partial import strawberry_django from .pagination import StartIdPaginationInput, StartIdStrawberryDjangoField netbox_field = partial( strawberry_django.field, pagination=StartIdPaginationInput, field_cls=StartIdStrawberryDjangoField, ) ``` ```python from netbox.graphql.fields import netbox_field foo_list: list[FooType] = netbox_field() ``` ### Use case The current pagination approach relies on a relative offset. This struggles with very large result sets, because the offset must be calculated by scanning the entire table up to the offset position. Employing an object's unique primary key as a cursor avoids this, as we can leverage simple numeric comparison to filter for all PKs greater than or equal to a given value. Combined with the existing `limit` capability, this affords very efficient pagination at the cost of a fixed (and not very meaningful) ordering. ### Database changes N/A ### External dependencies N/A
MrUnknownDE added the type: featurecomplexity: highnetboxtype: featurestatus: acceptedtype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featuretype: featurecomplexity: highcomplexity: highcomplexity: highcomplexity: highcomplexity: highcomplexity: highcomplexity: highcomplexity: highstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptedstatus: acceptednetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetbox labels 2026-04-05 16:56:48 +02:00
Sign in to join this conversation.
No Label complexity: high complexity: high complexity: high complexity: high complexity: high complexity: high complexity: high complexity: high complexity: high netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature type: feature
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/netbox#625