Skip to main content

Limits and quotas

AdministratorsIT operationsSupport
In one minute

Every tenant has a budget: how large one request may be, how many requests a minute it may send, how much it may ingest in a day, how many live connections it may hold open, and, once you switch it on, how large it may grow in total.

The defaults ship with the API and apply to every tenant. Any single tenant can be raised, or made unlimited, with one database update that takes effect within five minutes and needs no restart. That is the mechanism behind "a customer has asked for their limit to be lifted".

The five kinds of limit

KindBoundsWhat the caller gets backClears
Request sizeHow large one request body may beToo large, 413On a smaller request
RateRequests a minute, per tenant and per userToo many requests, 429, with a Retry-After headerAt the end of the current minute
Daily quotaHow much may be ingested in one dayToo many requests, 429, Retry-After pointing at midnight00:00 UTC
Lifetime ceilingHow large the tenant may grow in total, off until switched onRefused, 403, saying the limit has to be raisedNever, waiting does not help
Live connectionsOpen connections, and subscriptions on eachAn error message naming the limit. Over the connection cap the connection is closed; over the per-connection subscription cap only that subscription is refusedWhen a connection is released

The distinction that matters when a support ticket arrives: a "too many requests" refusal clears on its own, a "raise the limit" refusal does not. The first is a customer sending faster than their budget allows, the second is a customer who has outgrown it.

The defaults, and the column that overrides each one

Deployment-wide defaults live under datahub.limits in the API's configuration. Each one can be overridden for a single tenant by setting the matching column in that tenant's tenant_limits table.

Setting under datahub.limitsDefaultPer-tenant columnWhat it bounds
max-body-bytes4 MiBdeployment-wide onlyThe largest accepted request body
max-body-bytes-datapoints16 MiBdeployment-wide onlyThe same, for measurement writes, which are legitimately larger
cache-ttl5mdeployment-wide onlyHow long an override takes to reach every instance
rate.write-per-minute-per-tenant2,000write_per_minute_per_tenantWrites a minute for the whole tenant
rate.read-per-minute-per-tenant6,000read_per_minute_per_tenantReads a minute for the whole tenant
rate.write-per-minute-per-user600write_per_minute_per_userWrites a minute for one identity
rate.read-per-minute-per-user1,200read_per_minute_per_userReads a minute for one identity
quota.events-per-day100,000events_per_dayEvents created in a day
quota.nodes-per-day50,000nodes_per_dayEntities created in a day, see the note below
quota.edges-per-day100,000edges_per_dayRelationships created in a day
quota.datapoints-per-day10,000,000datapoints_per_dayMeasurements written in a day
quota.ingest-bytes-per-day1 GiBingest_bytes_per_dayBytes of write traffic in a day
lifetime.max-resources1,000max_resourcesEntities held at once, see the note below
lifetime.max-events-total25,000max_events_totalEvents ever created
lifetime.max-datapoints-total1,000,000,000max_datapoints_totalMeasurements ever written
lifetime.max-text-datapoints-total100,000max_text_datapoints_totalText measurements ever written
websocket.max-sockets-per-tenant10max_ws_sockets_per_tenantLive connections open for the tenant
websocket.max-sockets-per-user10max_ws_sockets_per_userLive connections open for one identity
websocket.max-subscriptions-per-socket10deployment-wide onlySubscriptions multiplexed onto one connection

Each group has an on/off switch. rate.enabled, quota.enabled and websocket.enabled are all on by default. lifetime.enabled is off by default, because those figures size a small evaluation tenant rather than a real deployment.

Grant the exemptions before switching lifetime ceilings on

Enabling them applies them to every tenant that has no override, which is every tenant you have not already touched. One already holding more than max-resources starts refusing writes at the next restart, with no warning beforehand.

The order that avoids that: put a 0 in the tenant_limits row of every tenant that should keep growing first, then set lifetime.enabled: true.

Four things about that table are easy to read wrong:

  • "Entities" is broader than "resources". Resources, time series, data sets, labels, policies and functions are all rows in the same table, so nodes-per-day and max-resources cover all of them together rather than resources alone.
  • A lifetime ceiling is a size, not a rate. Entities are counted live, so deleting one frees room again. Events and measurements accumulate, so deleting them does not. Create-and-delete churn cannot reset those two.
  • Read and write are not simply fetching and posting. Plenty of operations that only read are submitted as posts, because they carry a filter or a long list of ids in the body. Searching, filtering, counting, listing and fetching neighbours are charged to the read budget even so; the exact list is in the developer documentation. Everything else that is not a plain fetch counts as a write, deletions included. So a person browsing the console spends the read allowance, which is the larger of the two.
  • Nothing is exempt. The administrator role, the console's own traffic and the agent tools are all charged the same way, and the console spends the signed-in user's budget rather than a budget of its own.

Quotas are counted per instance and reconciled continuously, so a tenant can overshoot a daily allowance slightly on a multi-instance deployment. They are ceilings, not invoices.

Raising a limit for one tenant

Each tenant's schema holds a tenant_limits table with a single row, created and seeded when the tenant is provisioned. Every limit column starts empty, meaning inherit the deployment default. Setting one to 0 or a negative number means unlimited.

Run this in that tenant's own schema:

UPDATE tenant_limits
SET max_resources = 0,
note = 'paid plan, ticket 1234'
WHERE id = 1;

Within cache-ttl, five minutes by default, every API instance picks the change up. No restart, no redeployment and nothing to coordinate across instances. There is also no way to make it happen sooner, so tell the customer their limit is lifted in five minutes rather than immediately.

Always fill in the note

The note column is free text and exists for exactly this purpose: who asked, and why it was granted. A limit lifted a year ago with no note is a limit nobody can review, and the updated_at column beside it only tells you when.

The table is created by the platform's own database migrations, once per tenant schema, so there is nothing to install and a tenant starts out inheriting every default.

When the cache store is unavailable

Rate limits, daily quotas and the running totals behind the event and measurement ceilings are all counted in the key-value store. If it cannot be reached, they fail open: traffic is allowed through and a warning is logged, rather than the platform refusing requests it cannot account for.

Two things keep working regardless, because neither needs anything but the request and the database: the request size limits, and the lifetime ceiling on entities, which is counted live in the tenant's own database.

Two consequences worth planning for:

  • An outage of that store is not a security event, but a tenant can exceed its budget for the duration of it, and the overage is not recovered afterwards.
  • The event and measurement totals live only in that store, so flushing it resets them. The entity count, being read from the database, is unaffected.

At the reverse proxy

Two settings on the proxy in front of the API matter, and both are in the reverse-proxy examples that ship with the platform:

  • A per-IP request limit, 20 requests a second with a burst of 40. The platform's own limits are charged to a tenant or a user, so they can only act once a caller has been identified. Sign-in, token validation and connection handshakes all happen before that point, and the per-IP limit is what bounds them.
  • A body size ceiling of 20 MB, deliberately set above the API's own 4 MiB and 16 MiB caps. The point of the gap is that an oversized request should reach the application and come back as a readable message naming the limit, rather than as the proxy's bare error page. File uploads are exempt from it and stream without a ceiling.

What is not limited

There is no per-tenant quota on file storage. Uploads and downloads stream, so they sit outside the request size limit and outside the daily byte allowance as well, and nothing here refuses the upload that fills the disk. Until such a quota exists, file volume is bounded only by capacity planning, the housekeeping sweeps, and whatever ceiling the storage itself has. Data lifecycle →

Go deeper