Size limits
Two settings bound what one user can store.
| Setting | Default | What it bounds |
|---|---|---|
MAX_VALUE_BYTES | see below | Serialized size of a single preference value |
MAX_KEYS_PER_USER | 2000 | How many preferences one user may hold |
Both live in HABARICO_PREFERENCES, beside DEFAULTS:
HABARICO_PREFERENCES = {
"MAX_VALUE_BYTES": {
"default": 16 * 1024,
"preferences.dashboard": 32 * 1024,
r"[a-zA-Z][a-zA-Z0-9_./-]*\.savedQueries": 32 * 1024,
},
"MAX_KEYS_PER_USER": 2000,
}
MAX_VALUE_BYTES is resolved by the key — or the pattern string — a preference was registered
under, falling back to its "default" entry. A pattern entry is keyed by the same string passed
to register_preference_pattern.
It is read when a preference is registered, exactly like DEFAULTS. Registration happens in
AppConfig.ready(), after Django settings are loaded, so a value in settings.py always applies;
only a change made at runtime, after startup, does not move a limit already in place. In tests,
register inside the override_settings block rather than around it. MAX_KEYS_PER_USER belongs to
no single preference and is read on each write, so it has no such constraint.
The setting replaces the built-in dict rather than merging into it, so a project that overrides it
restates every entry it wants to keep — including "default".
The defaults sit an order of magnitude above what the frontend actually writes — 100 datagrid
columns are ~2 KB of columns or columnWidths, a 30-widget dashboard ~19 KB and 20 saved
queries ~11 KB at their fullest — so a project that stores unusually large preferences is the one
that has to raise them.
A key registered by the project takes the "default" entry unless it declares its own:
register_preference(
"preferences.eshop/order.importMapping",
schema=ImportMappingPreference,
max_bytes=256 * 1024,
)
Leaving max_bytes out — or passing None — means "not given", and the dict decides. There is no
sentinel for it, unlike the default of a preference: a size is never None, so None is free to
mean "not given".
None means "no limit" where a limit is actually configured — as one entry of the dict, as the
whole MAX_VALUE_BYTES, or as MAX_KEYS_PER_USER. A key that should be unlimited says so there:
HABARICO_PREFERENCES = {
"MAX_VALUE_BYTES": {
"default": 16 * 1024,
"preferences.eshop/order.importMapping": None, # unlimited
},
}
MAX_KEYS_PER_USER is checked only when a new key is created, so a user already past it can
still overwrite or shrink what they have. It is needed because the
pattern keys match an unbounded set of names: without it the per-value limit
would not bound the per-user total. Stored values are never re-checked on read, so lowering a
limit never deletes anything — that is the one case
self-healing deliberately leaves alone.
Both refusals are 422, the same response a schema mismatch gets. The oversized case is also
caught from Content-Length before the body is parsed, so a request already known to be too big
costs nothing to refuse. That measurement includes whitespace, while the registry measures the
compact JSON — a pretty-printed body just under the limit can therefore be refused, which is
deliberate: the bytes on the wire are the cost being bounded. Django's own
DATA_UPLOAD_MAX_MEMORY_SIZE (2.5 MB) still applies above these limits: it bounds parsing, these
bound storage.
A refused write is silent in the admin UI. The frontend store mirrors preferences to the backend
fire-and-forget: the value stays in the local store, so it looks applied until the page is reloaded
and the server state is hydrated back over it. Pass onError to
createBackendStore to surface
it to the user.
See also
- Registering a preference — where
max_bytesis passed - Pattern keys — the unbounded key set the count limit exists for
- User preferences — the Ruby counterpart, setting for setting