---
title: "Limits and errors"
description: "OQL hard limits, the row cap, the timeout, and the error categories you can expect."
canonical_url: https://developer.onepagecrm.com/oql/limits-and-errors/
source: Markdown mirror of https://developer.onepagecrm.com/oql/limits-and-errors/
---

OQL has a small set of hard limits, enforced before a query runs. When
your query exceeds one, you get a typed error message instead of a
partial or truncated result.

## Limits

| Limit | Value | Notes |
|-------|-------|-------|
| Query timeout | Long-running queries are cut off | Server-enforced; returns `ExecutionError` |
| Max `limit` | 1000 rows | Values above are rejected, not clamped |
| Max `group_by` fields | 3 | Duplicate fields are rejected |
| `like` wildcards per pattern | 3 | `%` and `_` both count |
| `like` pattern length | 100 characters | Null bytes are rejected |
| `DAYS_AGO` argument | ±36500 | Roughly ±100 years |
| Distinct value set | 16 MB per group | Distinct aggregates and `distinct: true` collect unique values in memory (roughly 700k IDs) |

### Why limits are strict, not soft

A silently truncated result looks complete, so anything you compute
from it is quietly wrong. OQL never does that. An over-limit query
raises a validation error, and hitting the row cap sets
`truncated: true` on the result:

```jsonc
{
  "rows": [ /* up to your limit */ ],
  "row_count": 100,
  "truncated": true
}
```

`truncated: true` means more rows exist beyond the requested limit.
Raise `limit` (up to 1000) or refine `where`.

## Error categories

OQL raises two kinds of error.

### `ValidationError`

The query is malformed or references something that does not exist.
Caught before the query runs. Examples:

```text
Unknown entity 'widgets'
Unknown field 'foo' on entity 'contacts'
Field 'background' is not filterable
Field 'first_name' is not sortable
Operator '<' is not compatible with type 'string' on field 'first_name'
'limit' must be <= 1000, got 5000
'like' pattern has 5 wildcards, max is 3
'group_by' has 4 fields, max is 3
Invalid date '2026-13-99' for 'created_at' (expected ISO 8601)
'between' value must be a 2-element array
Multi-operator condition on field 'amount' is not supported
```

Reference resolution failures are also `ValidationError`s and point
you to where to look up valid values:

```text
Invalid status_id 'foo'. Valid values: lead, prospect, customer, ...
Invalid owner_id '507f...'. Must be a valid user ID.
Invalid pipeline_id '507f...'. Use context() to see valid pipelines.
```

### `ExecutionError`

The query was well-formed but failed at runtime. Almost always a
timeout:

```text
Query timed out (10000ms limit)
```

Timeouts usually indicate a `where` that returns far too many rows
before filtering kicks in. Add a more selective filter (a date range,
an `owner_id`, a `status`), or reduce the scope by querying a
narrower entity.

Grouping or distinct-collecting over too many unique values can also
exceed the aggregation memory limit:

```text
Query exceeded the aggregation memory limit. Narrow it with a where
filter or fewer groups before aggregating.
```

For high-cardinality fields like `id` or `contact_id` over large
collections, prefer `count()` (a plain row count) over
`count(distinct id)`, or scope the query with `where` first.

## Reducing the chance of timeouts

- Filter on selective fields first. `owner_id`, `assignee_id`,
  `contact_id`, `status`, `status_id`, `pipeline_id`, and the timestamp
  fields (`created_at`, `modified_at`, `close_date`, `date`) narrow the
  result set quickly and are good starting points.
- Prefer `=` and `in` over `like` for known values.
- Use date functions (`THIS_QUARTER()`, `DAYS_AGO`) to bound time
  ranges; running over an unbounded history is the most common
  source of slow queries.
- When aggregating, narrow with `where` first. `group_by` works on
  whatever survives the filter.
