Cache ObjectType results for the duration of a request #497

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

Originally created by @jeremystretch on 1/22/2026

NetBox Version

v4.5.1

Python Version

3.12

Area(s) of Concern

  • User Interface
  • REST API
  • GraphQL API
  • Python ORM
  • Other

Details

Prologue

In regular Django, the ContentType for a model is typically resolved using the get_for_model() method on ContentTypeManager, e.g.

ContentType.objects.get_for_model(Site)

ContentTypes are cached on the manager itself for the duration of the Python process to avoid redundant database queries, as ContentTypes are not expected to change. (Each is tied to a model defined in code.)

To support dynamic custom objects and to track model-specific features, NetBox implements its own ObjectType class as a wrapper around Django's ContentType:

ObjectType.objects.get_for_model(Site)

This works the same way, however the result of the query is not cached, because we must allow for the possibility that custom object types are added or deleted throughout the life of a process.

Problem

Although we cannot cache these results long-term, it is considered safe to cache them for the duration of a single request. As ObjectType resolutions are quite common, even a short-term caching such as this could eliminate a substantial number of redundant queries.

For example, a REST API call to create 100 IP addresses each assigned to an interface currently triggers around 700 redundant queries:

SELECT ••• FROM "core_objecttype" INNER JOIN "django_content_type" ON ("core_objecttype"."contenttype_ptr_id" = "django_content_type"."id") WHERE ("django_content_type"."app_label" = 'ipam' AND "django_content_type"."model" = 'ipaddress') LIMIT 21

While these individual queries are negligible in terms of performance impact, their aggregate impact is substantial enough to warrant optimization, if only to reduce the overall number of SQL queries being executed.

Proposal

My idea is to implement a new ContextVar to serve as a cache mapping models to their resolved ObjectTypes. This cache will be cleared at the end of each request by the event_tracking() context manager, similar to how we manage the events queue.

from contextvars import ContextVar

'request_cache = ContextVar('request_cache', default=dict())

Although this proposal targets ObjectTypes specifically, I suggest keeping the implementation generic so that it can be used for other applications, such as custom field retrieval. (I intend to open a separate issue for that work if this initial implementation proves viable.)

We can introduce a decorator to cleanly populate the cache as ObjectTypes are resolved:

@cache_for_request('object_types')
def get_for_model(self, model, for_concrete_model=True):
    ...

This would return the cached result if available, and otherwise issue the database query and cache the result. We'll need to decide how to handle the for_concrete_model conditional: We could index for both True and False values, but the False condition seems uncommon enough that we might just bypass the cache for it.

Note

I initially considered using an LRU cache for this, however I don't think there's a reliable hook to invalidate the cache outside of a request/response workflow. (This would break support for dynamic custom objects.)

*Originally created by @jeremystretch on 1/22/2026* ### NetBox Version v4.5.1 ### Python Version 3.12 ### Area(s) of Concern - [x] User Interface - [x] REST API - [x] GraphQL API - [ ] Python ORM - [ ] Other ### Details ### Prologue In regular Django, the ContentType for a model is typically resolved using the `get_for_model()` method on ContentTypeManager, e.g. `ContentType.objects.get_for_model(Site)` ContentTypes are cached on the manager itself for the duration of the Python process to avoid redundant database queries, as ContentTypes are not expected to change. (Each is tied to a model defined in code.) To support dynamic [custom objects](https://github.com/[netboxlabs/netbox-custom-objects](https://github.com/netboxlabs/netbox-custom-objects)) and to track model-specific features, NetBox implements its own ObjectType class as a wrapper around Django's ContentType: `ObjectType.objects.get_for_model(Site)` This works the same way, however the result of the query is _not_ cached, because we must allow for the possibility that custom object types are added or deleted throughout the life of a process. ### Problem Although we cannot cache these results long-term, it is considered safe to cache them for the duration of a single request. As ObjectType resolutions are quite common, even a short-term caching such as this could eliminate a substantial number of redundant queries. For example, a REST API call to create 100 IP addresses each assigned to an interface currently triggers around 700 redundant queries: `SELECT ••• FROM "core_objecttype" INNER JOIN "django_content_type" ON ("core_objecttype"."contenttype_ptr_id" = "django_content_type"."id") WHERE ("django_content_type"."app_label" = 'ipam' AND "django_content_type"."model" = 'ipaddress') LIMIT 21` While these individual queries are negligible in terms of performance impact, their aggregate impact is substantial enough to warrant optimization, if only to reduce the overall number of SQL queries being executed. ### Proposal My idea is to implement a new [ContextVar](https://docs.python.org/3/library/contextvars.html) to serve as a cache mapping models to their resolved ObjectTypes. This cache will be cleared at the end of each request by the [`event_tracking()`](https://github.com/netbox-community/netbox/blob/077d9b11294028b5e444a4c088058d18008533db/netbox/netbox/context_managers.py#L10) context manager, similar to how we manage the events queue. ```python from contextvars import ContextVar 'request_cache = ContextVar('request_cache', default=dict()) ``` Although this proposal targets ObjectTypes specifically, I suggest keeping the implementation generic so that it can be used for other applications, such as custom field retrieval. (I intend to open a separate issue for that work if this initial implementation proves viable.) We can introduce a decorator to cleanly populate the cache as ObjectTypes are resolved: ``` @cache_for_request('object_types') def get_for_model(self, model, for_concrete_model=True): ... ``` This would return the cached result if available, and otherwise issue the database query and cache the result. We'll need to decide how to handle the `for_concrete_model` conditional: We could index for both True and False values, but the False condition seems uncommon enough that we might just bypass the cache for it. > [!NOTE] > I initially considered using an LRU cache for this, however I don't think there's a reliable hook to invalidate the cache outside of a request/response workflow. (This would break support for dynamic custom objects.)
MrUnknownDE added the type: performancestatus: acceptedtype: performancetype: performancenetboxcomplexity: mediumtype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancetype: performancestatus: 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: acceptedcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumcomplexity: mediumnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetboxnetbox labels 2026-04-05 16:37:23 +02:00
Sign in to join this conversation.
No Label complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium complexity: medium netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox 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 type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance type: performance
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/netbox#497