Docs

Replica Cache

Read-only REST API serving NetBox data from an independent cache

Replica Cache is a read-only REST API that serves NetBox data from an operationally independent cache, delivering sub-50ms P95 query latency and resilience independent of the primary NetBox database. It is available as a premium-tier feature on NetBox Cloud.

Customer Preview: Replica Cache is in customer preview. The API surface may evolve in future releases.

For the interactive API reference (OpenAPI/ReDoc), visit /docs on your Replica Cache endpoint.


Why Use Replica Cache

  • Operational resilience - Replica Cache serves reads from an operationally independent cache. Your monitoring, reporting, and automation scripts keep working even during NetBox maintenance windows or outages.
  • Fast reads at scale - Queries return in single-digit milliseconds for most workloads. Filtering and pagination happen server-side, reducing data transfer and client-side processing.
  • Zero load on the primary database - Every Replica Cache read is served from the cache. High-frequency polling, dashboards, and bulk data exports never touch the primary NetBox database.

Replica Cache complements the primary NetBox API - use it for read-heavy automation that benefits from speed and resilience, and continue using the primary API for writes and real-time consistency.

Replica Cache vs. TurboBulk: Both are premium-tier performance features. TurboBulk provides fast bulk reads and writes with exact consistency, operating directly against the primary database. Replica Cache offloads reads to an independent cache - eventually consistent, but zero primary database load and resilient to outages. They're complementary: TurboBulk for bulk writes and consistent reads, Replica Cache for high-frequency read automation and resilience.


Key Differences from the NetBox API

AspectNetBox APIReplica Cache API
Base path/api//replica-cache/v1/
Write supportFull CRUDRead-only (GET)
Response formatNested objects (e.g. nested site on device)Flat rows (foreign keys are IDs)
PaginationOffset-based (limit/offset)Cursor-based (limit/cursor)
Filter syntax?name=foo?filter[name]=foo
BranchingSupports branchesMain branch only
ConsistencyReal-timeNear-real-time - changes stream in within seconds (typically 1-2s)
AuthenticationAuthorization: Bearer <key>Authorization: Bearer <key> + NBC-Netbox-ID header

Replica Cache is not a drop-in replacement for the NetBox API. Responses use flat column values rather than nested objects, filter syntax differs, and write operations are not supported.


Authentication

Every request to /v1/* endpoints requires two headers:

HeaderValueDescription
AuthorizationBearer <token>Replica Cache API token issued by NetBox Labs for your tenant
NBC-Netbox-ID<tenant-id>Your NetBox Cloud tenant ID, found in the Cloud console. Must match the tenant the API token was issued for - mismatches are rejected with 403
BASE_URL="https://<your-netbox>.cloud.netboxapp.com/replica-cache"

curl -s \
  -H "Authorization: Bearer $REPLICA_CACHE_API_TOKEN" \
  -H "NBC-Netbox-ID: $TENANT_ID" \
  "$BASE_URL/v1/dcim/devices?limit=1"

Replica Cache uses tenant-bound API tokens that are separate from your NetBox Cloud API token - contact NetBox Labs Support to have one issued for your tenant. The token is permanently bound to a single tenant_id, so the value you send in NBC-Netbox-ID must match the tenant the token was issued for. If they disagree the server returns 403; if NBC-Netbox-ID is missing the server returns 400; if the token is unknown the server returns 401.


Quick Start

1. Choose an entity

Entity coverage is published in the OpenAPI reference at /docs. Entity paths use {app}/{model} style, for example dcim/devices.

2. List records

Query any entity using its {app}/{model} path (derived from the table name - dcim_device becomes dcim/devices):

curl -s \
  -H "Authorization: Bearer $REPLICA_CACHE_API_TOKEN" \
  -H "NBC-Netbox-ID: $TENANT_ID" \
  "$BASE_URL/v1/dcim/devices?limit=2"
{
  "count": 1042,
  "data_as_of": "2026-09-04T19:58:12Z",
  "next_cursor": "<opaque cursor string>",
  "results": [
    { "id": 1, "name": "spine-01", "status": "active", "site_id": 5 },
    { "id": 2, "name": "spine-02", "status": "active", "site_id": 5 }
  ]
}

3. Get a single record

Fetch one record by its primary key:

curl -s \
  -H "Authorization: Bearer $REPLICA_CACHE_API_TOKEN" \
  -H "NBC-Netbox-ID: $TENANT_ID" \
  "$BASE_URL/v1/dcim/devices/1"
{ "id": 1, "name": "spine-01", "status": "active", "site_id": 5 }

Querying

Filtering

Filter results using query parameters in the form filter[column]__operator=value. When no operator is specified, eq (exact match) is used.

# Devices with status "active" at site 5
curl -s \
  -H "Authorization: Bearer $REPLICA_CACHE_API_TOKEN" \
  -H "NBC-Netbox-ID: $TENANT_ID" \
  "$BASE_URL/v1/dcim/devices?filter[status]=active&filter[site_id]=5"

Operators:

OperatorSyntaxDescriptionExample
eqfilter[col]=val or filter[col]__eq=valExact match (default)filter[status]=active
infilter[col]__in=a,b,cMatch any value in comma-separated listfilter[status]__in=active,staged
ilikefilter[col]__ilike=valCase-insensitive substring match. Text columns only; on any other column the request is rejected with 400. %, _ and \ in the value match literally.filter[name]__ilike=spine
isnullfilter[col]__isnull=trueCheck for NULL (true) or NOT NULL (false)filter[serial]__isnull=false
gtfilter[col]__gt=valGreater thanfilter[id]__gt=100
ltfilter[col]__lt=valLess thanfilter[id]__lt=500

A deployment may also declare filters that are not columns of the entity's own table. /v1/_meta/schema lists them per entity under virtual_filters. One of those reaches a second table, and a generic-relation filter reaches a third through django_content_type. If either has received no data on your instance, the filter cannot be applied. The route then answers 404 no data received for this entity rather than an empty 200, because an empty 200 would claim that nothing matched.

Column names correspond to database columns. Available columns vary by NetBox version and configuration; unknown columns are rejected with 400. A filter value the column's type cannot accept is also rejected with 400, as is a malformed percent escape anywhere in the query string - percent-encode a literal % as %25.

Sorting

Sort results with the sort parameter. Prefix with - for descending order:

?sort=name          # ascending by name
?sort=-id           # descending by id

Default sort order is ascending by primary key.

Field Selection

Return only specific columns with the fields parameter:

?fields=id,name,status

The primary key column is always included in the response, even if not listed. Under expand, each resolved reference's raw <key>_id comes too: a resolved name is null both when the key is null and when it points at a row your replica has not received, and the id beside it is what tells you which.

fields selects base-table columns. It does not list or suppress a column added by expand - see below.

Resolving foreign keys: expand

A row carries foreign keys as ids. expand names the ones you want resolved to a readable value, and adds them as flat columns beside the id:

curl -s \
  -H "Authorization: Bearer $REPLICA_CACHE_API_TOKEN" \
  -H "NBC-Netbox-ID: $TENANT_ID" \
  "$BASE_URL/v1/dcim/devices?expand=site,role&sort=site"
{ "results": [{ "id": 42, "site_id": 2, "site": "DM-Akron", "site_slug": "dm-akron" }] }

The bare <key> carries the first declared target column. Any further one arrives as <key>_<column>. The raw <key>_id is untouched.

expand is opt-in. Without it the response is exactly what it was before, and no join runs.

Naming a resolved column in fields is accepted and selects nothing - expand already added it - so you can echo back the keys a response gave you. A name that degrades on your replica is accepted the same way, so one stable fields list works across replicas that differ.

expand applies to list routes only. The single-record route GET /v1/{app}/{model}/{id} ignores it and returns the row unchanged.

expand unlocks sorting and filtering on the resolved name. ?expand=site&sort=site orders by the site name; ?expand=site&filter[site]__ilike=akron filters on it. Without expand=site, sort=site returns 400 unknown sort column: site - an expanded name is a column for every purpose, and an unexpanded one does not exist.

One filter is refused on a resolved name: __isnull=true. A null name means either that the foreign key is null or that it points at a row your replica has not received, and a row set cannot show you which one you got. filter[site]__isnull=true returns 400 naming the alternative. Use filter[site_id]__isnull=true, which asks whether the device has a site and nothing else.

filter[site]__isnull=false is allowed, and it is the way to ask for rows whose reference resolved. filter[site_id]__isnull=false is not the same question - it also matches a row whose site_id points at a site your replica has not received.

When a reference cannot resolve

A declared name can still fail to resolve on your replica, and it then degrades: the affected column is absent from the row rather than present and null, and the raw <key>_id still answers. Test for the key's presence, not for a null value.

Five cases produce it:

caseeffect
Your replica does not hold the target tableevery column of that key is absent
Your replica holds the target table but has received no data for itevery column of that key is absent
The target table is here but carries no readable name columnevery column of that key is absent
The target table is here and carries the name but not a further columnonly that <key>_<column> is absent; the bare <key> still resolves
The entity itself carries no such foreign-key column on your NetBox versionevery column of that key is absent

A target that has received no data answers 404 on its own route, so resolving it would put a null name on every row. The column is dropped instead.

Sorting or filtering on a name in any of those states returns 400 giving the reason, and the reason says which case applies. A missing column is visible in the rows; a changed ordering or a changed result set is not, so the API refuses rather than quietly serving one.

Check availability before you query. GET /v1/_meta/schema reports, per column, the reference it carries and whether this replica can resolve it (references.available). That is the way to find out which keys work for you - a degraded expansion looks the same on the wire as one you never asked for.

A name no reference is declared for returns 400. Which names an entity accepts is fixed per NetBox version, not per replica.

Cost

Each page under expand scans and joins the entity's table. The keyset predicate reads the joined column, so it sits above the join and cannot skip it - the sort is bounded, the scan is not. Measured on a 6.8M-row dcim_device:

requestlatency
deep page, no expand9-11 ms
expand=site12-16 ms
expand=site&sort=site29-37 ms
12 expansions on one request100-115 ms

The sorted join is never held in memory in full, so a deep page costs the same as a shallow one. Plan for tens of milliseconds per page, well inside the 10-second request timeout.


Pagination

Replica Cache uses cursor-based pagination. Each list response includes:

FieldDescription
countTotal number of rows matching your filters
data_as_ofRFC3339 UTC instant this cache's contents are current to for the entity; null when that instant is unknown, including for the whole of the instance's first load
next_cursorOpaque cursor string for the next page; null on the last page
resultsArray of records for the current page

Use the limit parameter to control page size (default: 50, maximum: 1000). Every deployment enforces the same maximum. A larger limit returns 400 naming that maximum, rather than a silently shortened page.

Send each query parameter at most once. A repeated limit, cursor, fields, sort, expand or filter parameter returns 400.

To paginate through all results, pass the next_cursor value from each response as the cursor parameter on the next request:

cursor=""
while true; do
  response=$(curl -s \
    -H "Authorization: Bearer $REPLICA_CACHE_API_TOKEN" \
    -H "NBC-Netbox-ID: $TENANT_ID" \
    "$BASE_URL/v1/dcim/devices?limit=100${cursor:+&cursor=$cursor}")

  # Process results...
  echo "$response" | jq '.results[]'

  # Get next cursor; exit loop if null (last page)
  cursor=$(echo "$response" | jq -r '.next_cursor // empty')
  [ -z "$cursor" ] && break
done

Data Freshness

Replica Cache is fed by a continuous stream of changes from the primary NetBox database. Every create, update, and delete is propagated to the cache as it happens - not on a fixed polling interval - so a change made in NetBox typically appears in the cache within 1-2 seconds, and nearly always within 10 seconds.


Schema

Replica Cache schema depends on your NetBox version and installed plugins. Row payloads are JSON objects whose keys mirror the cached database columns for that entity. Build filters and field selections against columns present in returned rows; requests for unknown filter, sort, or field-selection columns return 400. Names added by expand count as columns for filtering and sorting once expanded.

Reading the column catalogue

One call returns every entity's columns for your instance, so you do not have to infer them from returned rows:

GET /v1/_meta/schema
{
  "snapshot_complete": true,
  "entities": {
    "/v1/dcim/devices": {
      "table": "dcim_device",
      "primary_key": "id",
      "ingested": true,
      "data_as_of": "2026-09-04T19:58:12Z",
      "columns": [
        { "name": "id", "type": "BIGINT", "nullable": false,
          "operators": ["eq", "gt", "lt", "in", "isnull"] },
        { "name": "name", "type": "VARCHAR", "nullable": true,
          "operators": ["eq", "gt", "lt", "in", "isnull", "ilike"] },
        { "name": "site_id", "type": "BIGINT", "nullable": true,
          "operators": ["eq", "gt", "lt", "in", "isnull"],
          "references": { "path": "/v1/dcim/sites", "expand_key": "site",
                          "columns": ["name", "slug"], "available": true } }
      ]
    },
    "/v1/dcim/racks": {
      "table": "dcim_rack",
      "primary_key": "id",
      "ingested": false,
      "data_as_of": null,
      "columns": []
    },
    "/v1/core/object-types": {
      "table": "core_objecttype",
      "primary_key": "contenttype_ptr_id",
      "ingested": true,
      "data_as_of": "2026-09-04T19:58:12Z",
      "columns": [
        { "name": "contenttype_ptr_id", "type": "INTEGER", "nullable": false,
          "operators": ["eq", "gt", "lt", "in", "isnull"] },
        { "name": "public", "type": "BOOLEAN", "nullable": true,
          "operators": ["eq", "gt", "lt", "in", "isnull"] },
        { "name": "features", "type": "VARCHAR", "nullable": true,
          "operators": ["eq", "gt", "lt", "in", "isnull", "ilike"] }
      ]
    }
  }
}

The response reads your instance's own catalogue at request time, so it tracks your NetBox version rather than a build-time constant.

FieldMeaning
snapshot_completeWhether your instance's initial load had finished when the response read the flag. While the load is running, every entity reports a null data_as_of for that one reason. A false flag does not mean every data_as_of in the response is null - see below.
entitiesKeyed by route path. Every entity your instance serves appears.
tableThe underlying table name.
primary_keyThe column the single-record route addresses a row by, so you can build <path>/<value> from a row. Almost always id - but read it rather than assuming, because /v1/core/object-types is keyed by contenttype_ptr_id.
ingestedWhether any data has ever arrived for this entity. When it is false, that entity's list route returns 404.
data_as_ofWhen it holds a timestamp, the same value with the same meaning as the list envelope's data_as_of. null means the instant is unknown, never that the entity holds no data.
columnsThe entity's columns in table order, not alphabetical order. Empty whenever ingested is false.
name, typeThe column and its type.
nullableWhether the column may hold null. It does not say that it does.
operatorsThe filter operators the column accepts. eq, gt, lt, in and isnull apply to every column; ilike applies to VARCHAR columns only.
referencesWhere a foreign-key column points. Absent on a column that is not a resolvable foreign key. available is false when your instance has received no data for the target entity.
virtual_filtersFilter names this entity accepts that are not columns of its own table. Absent unless your deployment declares any.

A virtual filter reads its join table, and a generic-relation filter reads django_content_type as well. When any of them has received no data on your instance, the filter cannot be applied. The route then answers 404 no data received for this entity. An empty 200 would be indistinguishable from a filter that ran and matched nothing. The 200 returns once every table the filter reads has been ingested.

operators tells you which filters a column accepts. It does not promise a value will parse - a well-formed operator can still return 400 for a malformed value.

Reading an unknown data_as_of

Four states produce a null data_as_of. The response names two of them:

  • snapshot_complete is false. Your instance was still doing its initial load when the response read the flag, and every entity reports null until it finishes.
  • ingested is false. No data has ever arrived for that entity, so there is no instant to report and its list route returns 404.

The other two report snapshot_complete: true and ingested: true beside the null, and nothing in the response separates them:

  • The entity's data arrived before Replica Cache started recording arrival instants.
  • The entity's data arrived without a source timestamp attached.

In both cases the entity gains an instant on its next change.

Trust an instant whenever you are given one. snapshot_complete is read before the per-entity state, so an initial load that finishes mid-request can leave the flag false beside an entity that already carries a real instant.

snapshot_complete does not return to false if your instance is re-loaded later. It means an initial load has finished at some point, not that no load is running now.


Error Handling

Errors are returned as JSON with an error field:

{ "error": "missing tenant header" }

Status codes:

CodeMeaning
200Success
400Bad request - invalid filter, cursor, sort column, or missing parameter
401Unauthorized - missing or invalid API token
404Not found - unknown endpoint, table, or record; or no data received for the entity or a table its filter reads
405Method not allowed - only GET requests are supported
500Internal server error - query execution failed

Limitations

  • Read-only - no create, update, or delete operations. Use the primary NetBox API for writes.
  • Near-real-time, eventually consistent - changes stream from NetBox continuously and typically appear within 1-2 seconds (nearly always within 10s), but Replica Cache is not transactionally consistent with the primary. Do not rely on it for read-after-write consistency.
  • Main branch only - Replica Cache serves data from the main NetBox branch. Branching plugin branches are not available.
  • Flat responses - related objects are represented by foreign key IDs. Use expand to resolve a reference to a readable name server-side; the result is flat columns (site, site_slug), never a nested object.
  • Generic relations return raw identifiers - some NetBox columns come in pairs. One names the object type and the other names the row. dcim/cable-terminations is the example: termination_type_id gives the type and termination_id gives the row. Replica Cache returns both values unchanged. expand cannot resolve them, because the target table varies per row and the table that holds object-type names is not replicated. core/object-types does not fill this gap - see the next point. A record tells you that a cable terminates and on which cable_end, but not what kind of object it terminates on. Use the primary NetBox API when you need the resolved object.
  • core/object-types carries flags, not names - this endpoint serves each object type's public flag and its features list, keyed by contenttype_ptr_id. features reaches you as a JSON string, not a JSON array. The replication pipeline stores PostgreSQL array columns as text, and the column reports its type as VARCHAR. Parse it before you iterate it. Each parsed element is then a plain string or null, so the value reads ["custom-fields", "tags"]. Check for null before you use an element. Older rows still carry a different shape. Before this release the pipeline wrapped each element in an object keyed by its source type, so ["tags"] read [{"string": "tags"}]. A row keeps that wrapper until its next update rewrites it, and DATA-373 rewrites the rows that never update. Until then, read element.string on an element that arrives as an object. The endpoint does not carry app_label or model. So it cannot tell you what an object type is called, and it cannot resolve the *_type_id columns described above. Those names live in a Django table that the replication pipeline does not include. Read object-type names from the primary NetBox API.
  • extras/tags is published but not fed - the API reference lists this endpoint, but the replication pipeline does not include NetBox tags. The endpoint therefore never returns a tag record. That does not mean your NetBox has no tags. Read tags from the primary NetBox API, and contact NetBox Labs Support before you build on this endpoint.
  • Schema varies by NetBox version - available columns may change across NetBox upgrades. Requests for unknown columns return 400.
  • Non-core object types require configuration - core NetBox object types are available by default. Plugin models and non-core types can be enabled by contacting the NetBox Cloud support team.
  • Customer preview - the API surface may change in future releases.

API Reference

All endpoints below require the Authorization and NBC-Netbox-ID headers.

EndpointDescriptionKey Parameters
GET /v1/{app}/{model}List recordsfilter[col]__op, sort, fields, expand, limit, cursor
GET /v1/{app}/{model}/{id}Get a single record by primary key-
GET /v1/_meta/schemaList every entity's columns for your instance-

/v1/_meta/schema is the only served path whose segments begin with _. Every other _ path under /v1 is reserved and returns 404 after successful authentication.

For the full OpenAPI specification, visit /docs on your Replica Cache endpoint.

On this page