Skip to main content

Limits & quotas

The API enforces a handful of ceilings, and the status code says which one you hit and what to do about it. That is the whole design: what clears by waiting answers 429 and carries a Retry-After, and what does not answers something else. Retry the first kind, fix the second.

LimitStatusProblem typeHow it clears
Field caps400 / 422the usual validation bodyShorten the field
Batch caps400 / 422the usual validation bodySplit the batch
Request body size413.../errors/request-too-largeSplit the batch
Rate limit429 + Retry-After.../errors/rate-limit-exceededWait the seconds it names
Daily ingest quota429 + Retry-After.../errors/ingest-quota-exceededWait until 00:00 UTC
Lifetime ceiling403, no Retry-After.../errors/tenant-limit-reachedAsk for it to be raised

Every type above is prefixed https://intellistream.ai/errors/, and the 429 and 413 bodies are RFC 9457 problem documents served as application/problem+json. They reach you through the ordinary error path in each client (DatahubApiException in Java, DataHubException in Python, ResponseError in Rust); see Results & errors.

The numbers are defaults, not guarantees

Rate limits, quotas, ceilings and WebSocket caps are deployment policy: an operator sets them, and a tenant can be given its own. Lifetime ceilings are off altogether unless a deployment turns them on. Read the limit field off the response rather than hard-coding the value. The field and batch caps below are the wire contract and do not vary.

Field caps

Each of these bounds one entity. They apply on create and on update alike: the update forms enforce the same cap on set and on add, so you cannot grow past a cap one append at a time.

FieldCap
description10 000 characters
metadata256 entries, keys 128 characters, values 1 024 characters
labels64 labels, each at most 512 characters
relatedResources100 entries
geoLocation, as raw GeoJSON65 536 characters
Datapoint value64 characters

description is capped everywhere it appears: events, resources, time series, data sets, policies, functions and relationships.

Batch caps

PayloadCap
items in a DataWrapper10 000
Nodes and relations in POST /resources/create and /resources/update1 000 each
datapoints in one DatapointsCollection, numeric series100 000
datapoints in one DatapointsCollection, TEXT or MIXED series10 000

The items cap covers every endpoint taking the standard envelope: /events/create, /events/update, /events/delete, /timeseries/data, /timeseries/create, /edges/create, /datasets/create, /labels/create, /policies/create and /functions/create.

The tighter TEXT/MIXED cap is checked in the service, once the series' value type has been resolved, so it comes back naming the series type rather than the field. Split a text series into collections of 10 000 points or fewer.

The OpenAPI schema used to advertise caps nothing enforced

Some endpoints, /events/delete among them, carried a documented maximum in the schema that the runtime never checked, so an oversized batch went through. Those caps are enforced now. Code written against the advertised numbers is unaffected; code that quietly relied on them not being real is not.

Request body size

EndpointCap
POST /timeseries/data16 MiB
Everything else4 MiB
PUT /files and GET /files/download/**exempt, they stream
{
"type": "https://intellistream.ai/errors/request-too-large",
"title": "Request body too large",
"status": 413,
"detail": "The request body exceeds the 4194304 byte limit for this endpoint.",
"limitBytes": 4194304
}

A 413 is terminal. The same request will never become acceptable by being sent again, so split the batch instead of retrying it.

Rate limits

Counted per organisation and per user in a fixed one-minute window, with separate budgets for reads and writes.

Which budget a request spends follows what it does, not which method it uses. A GET is a read and a PUT, PATCH or DELETE is a write, but a POST that only reads because it carries a filter body is charged as a read: /events/filter, /resources/search, /timeseries/data/list, /timeseries/byids and every other endpoint whose last path segment is filter, search, byids, list, count, check, fetch-related, fetch-nearest, aggregate or latest. So a poller sitting on a filter endpoint budgets against the read allowance, which is the larger of the two.

ScopeWrites / minReads / min
Organisation2 0006 000
User6001 200
{
"type": "https://intellistream.ai/errors/rate-limit-exceeded",
"title": "Too many requests",
"status": 429,
"detail": "This tenant has used its 2000 requests per minute. Retry in 37 seconds.",
"scope": "tenant",
"limit": 2000,
"retryAfter": 37
}

scope is tenant or user, which tells you whether the noisy neighbour is you or your colleagues. Retry-After carries the same seconds as retryAfter, and never exceeds the 60 seconds left in the window.

The MCP tools at /mcp spend the same budget as REST: an agent and your ingest job share one allowance.

Daily ingest quotas

Per organisation, per UTC day, reset at 00:00 UTC. Retry-After points at that reset, so it can be hours.

metricDefault per day
events100 000
nodes50 000
relationships100 000
data points10 000 000
ingested bytes1 GiB of write-request body

nodes is the shared count of resources, time series, data sets, labels, policies and functions: they are one population, not five.

{
"type": "https://intellistream.ai/errors/ingest-quota-exceeded",
"title": "Ingest quota exceeded",
"status": 429,
"detail": "Daily events ingest quota (100000) is spent; it resets at 00:00 UTC.",
"metric": "events",
"limit": 100000,
"retryAfter": 43200
}

Lifetime ceilings

Totals, not rates: how large an organisation may grow. Unlike everything above, these are off unless a deployment turns them on, and the numbers size a free or trial organisation. Handle the 403, but do not plan your data model around these figures: ask whoever runs your deployment what applies to you.

metricCeiling where they are on
objects (resources, time series, data sets, labels and policies share it)1 000
events25 000
data points1 000 000 000
text data points, on TEXT / MIXED series100 000
{
"type": "https://intellistream.ai/errors/tenant-limit-reached",
"title": "Tenant limit reached",
"status": 403,
"detail": "This tenant has reached its limit of 25000 events. Contact IntelliStream to have it raised.",
"metric": "events",
"limit": 25000
}

There is deliberately no Retry-After: waiting does not clear a ceiling, and the status is 403 rather than 429 so no client mistakes it for one that does. The ceiling moves when someone raises it, which is a conversation with IntelliStream, not a retry.

Whether deleting helps depends on the metric:

MetricCountedDoes deleting free room?
objectsLive rowsYes
events, data pointsCumulativeNo

WebSocket caps

Both endpoints, /timeseries/datapoints/subscription/listen/** and /timeseries/datapoints/listen:

CapDefault
Concurrent connections per organisation10
Concurrent connections per user10
Subscriptions multiplexed over one socket10

Over the connection cap, the server sends one error frame naming the limit and then closes with 1008 (policy violation), so you get a reason instead of a bare close. Both sockets send the same shape, so one parser reads either:

{"error":true,"reason":"websocket-limit-reached","scope":"tenant","limit":10,"message":"..."}

Over the per-socket subscription cap, the socket stays open: the subscriptions already attached to it are still valid, and only the one that would not fit is refused.

{"error":true,"subscriptionExternalId":"engine_temps","reason":"subscription-limit-reached"}

It arrives on the same path as the other per-subscription refusals (forbidden, not-found), so a client that already handles those handles this one. See Subscriptions.

What the SDKs do about it

The split above lines up with what the ingest paths already do, so most of this needs no code from you:

ResponseRetried in processSpooled when buffering is on
429 (rate limit, daily quota), 5xx, network failureYes, with backoffYes
401, and 403 on a grantNoYes, until the credential is fixed
403 on a lifetime ceilingNoNo, surfaced to you
400 / 422 (validation), 413 (body too large)NoNo, surfaced to you

So rate limits and daily quotas take care of themselves: the client backs off and replays. A 413 or a validation failure reaches your code, which is the right place for it, since neither is fixed by trying again.

A lifetime ceiling is the one 403 that does not spool. The others are worth spooling because an expired token or a missing grant is fixed out of band and the data then flushes; a ceiling never becomes acceptable by being replayed, so buffering it would fill the spool with data the server refuses every time and bury the one message that says the limit is raised by asking. The client tells them apart on the problem type, so the ceiling surfaces on the call that hit it, in errors() on the IngestResult.

Two things to check in your own configuration:

  • batchSize defaults to 10 000, exactly the items cap. If you raised it, lower it back to 10 000 or below. See IngestOptions.
  • A TEXT or MIXED series batch must stay at or under 10 000 points per collection, whatever batchSize says. A numeric batch of 10 000 points is roughly 500 KB of JSON, comfortably inside the 16 MiB datapoint body cap.