# How Audit Limits Are Calculated and Enforced in OpenSEO

> Learn how OpenSEO calculates and enforces audit limits. Discover tiered limits, capacity units, and subscription validation to manage your SEO audits effectively.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-07-27

---

**OpenSEO enforces tiered audit limits by clamping page counts to plan boundaries, estimating total capacity units (pages + Lighthouse checks), and validating usage against subscription tiers before allowing new audits to start.**

The OpenSEO platform (available at `every-app/open-seo`) controls resource consumption during site audits through a multi-layered limiting system. This mechanism prevents infrastructure abuse while ensuring fair resource allocation across free and paid subscription tiers. Understanding exactly how audit limits are calculated and enforced helps API consumers handle quota errors gracefully and optimize their crawling strategies.

## Defining Static Bounds in audit-limits.ts

All numerical constraints originate in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts), which serves as the single source of truth for plan boundaries. This file exports constants that define the absolute minimum, default, and maximum page counts allowed per audit:

- `MIN_AUDIT_PAGES = 10`
- `DEFAULT_AUDIT_PAGES = 50`
- `FREE_MAX_AUDIT_PAGES = 50`
- `PAID_MAX_AUDIT_PAGES = 10_000`

These constants are imported across the codebase, ensuring that any change to plan limits propagates consistently to capacity calculations and enforcement logic.

## Calculating Audit Capacity

The [`src/server/features/audit/services/audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) module transforms static bounds into runtime capacity estimates through two primary functions: `clampAuditMaxPages` and `getEstimatedAuditCapacity`.

### Clamping User-Requested Page Counts

Before an audit begins, OpenSEO sanitizes the user-provided `maxPages` parameter using `clampAuditMaxPages`. This function guarantees the requested page count falls within absolute system limits regardless of the user's subscription tier:

```typescript
export function clampAuditMaxPages(maxPages?: number) {
  return Math.min(
    Math.max(maxPages ?? DEFAULT_AUDIT_PAGES, MIN_AUDIT_PAGES),
    PAID_MAX_AUDIT_PAGES,
  );
}

```

The logic ensures requests below 10 pages are raised to the minimum, unspecified values default to 50, and no request exceeds the hard ceiling of 10,000 pages.

### Estimating Total Capacity Units

OpenSEO tracks consumption using "capacity units" that combine page crawls with Lighthouse performance checks. The `getEstimatedAuditCapacity` function calculates this total workload:

```typescript
export function getEstimatedAuditCapacity(input: {
  maxPages?: number;
  lighthouseStrategy?: LighthouseStrategy;
}) {
  const pagesTotal = clampAuditMaxPages(input.maxPages);
  const lighthouseStrategy = input.lighthouseStrategy ?? "auto";
  const lighthouseChecks = lighthouseStrategy === "auto" ? 20 : 0;
  return {
    pagesTotal,
    lighthouseTotal: lighthouseChecks,
    total: pagesTotal + lighthouseChecks,
  };
}

```

When the Lighthouse strategy is set to `"auto"`, the system reserves an additional 20 capacity units per audit, ensuring sufficient resources are allocated for performance analysis.

## Runtime Enforcement in AuditService.ts

Actual limit validation occurs in [`src/server/features/audit/services/AuditService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts), which orchestrates the audit lifecycle and enforces tier-specific restrictions at runtime.

### Resolving the Subscription Tier

The service determines applicable limits via `resolveAuditLimitTier`. In hosted deployments, this checks the organization's subscription status; self-hosted instances default to the `"paid"` tier, effectively granting maximum resource allocation:

```typescript
const tier = await AuditService.resolveAuditLimitTier(organizationId);
const limits = AUDIT_LIMITS[tier];

```

### Validating Limits Before Execution

After clamping the page count, the service performs three sequential validations:

1. **Page count ceiling**: If `maxPages` exceeds `limits.maxPagesPerAudit` for the resolved tier, the service throws `AUDIT_PAGE_LIMIT_EXCEEDED`.

2. **Concurrent audit throttle**: The system queries current usage via `AuditRepository.getAuditUsageForUser` and rejects requests with `AUDIT_ALREADY_RUNNING` if `runningCount` exceeds `limits.maxRunningAudits`.

3. **Capacity unit quota**: If the estimated `capacityUnits` (pages + Lighthouse checks) exceeds `limits.maxCapacityUnits`, the service throws `AUDIT_CAPACITY_REACHED`.

```typescript
const maxPages = clampAuditMaxPages(input.maxPages);
if (maxPages > limits.maxPagesPerAudit) {
  throw new AppError("AUDIT_PAGE_LIMIT_EXCEEDED");
}

const reservation = getEstimatedAuditCapacity({ maxPages, lighthouseStrategy });
// ... audit row creation ...

const usage = await AuditRepository.getAuditUsageForUser(input.actorUserId);
if (usage.runningCount > limits.maxRunningAudits) {
  throw new AppError("AUDIT_ALREADY_RUNNING");
}
if (usage.capacityUnits > limits.maxCapacityUnits) {
  throw new AppError("AUDIT_CAPACITY_REACHED");
}

```

Notably, enforcement occurs **after** inserting the audit row, preventing race conditions where parallel requests might otherwise bypass limits.

## Summary

- **Free accounts** are restricted to 50 pages per audit, 1 concurrent audit, and 2,000 total capacity units.
- **Paid accounts** support up to 10,000 pages, unlimited concurrent audits, and 100,000 capacity units.
- The `clampAuditMaxPages` function enforces absolute bounds (10–10,000 pages) before tier-specific validation occurs.
- Capacity calculations include both page crawls and Lighthouse checks (20 units when strategy is `"auto"`).
- All enforcement logic resides in [`AuditService.ts`](https://github.com/every-app/open-seo/blob/main/AuditService.ts), which validates limits post-insertion to eliminate race conditions.

## Frequently Asked Questions

### What is the maximum number of pages allowed for a free audit?

Free-tier audits are capped at 50 pages per scan. The system enforces this via the `FREE_MAX_AUDIT_PAGES` constant in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts), and exceeding this value triggers an `AUDIT_PAGE_LIMIT_EXCEEDED` error during runtime validation.

### How does OpenSEO calculate capacity units for an audit?

Capacity units represent the sum of pages to be crawled plus Lighthouse checks. According to [`src/server/features/audit/services/audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts), each page counts as one unit, and audits using the `"auto"` Lighthouse strategy incur an additional 20 units. The total must remain below the tier-specific `maxCapacityUnits` threshold (2,000 for free, 100,000 for paid).

### What error codes indicate limit violations?

OpenSEO returns three specific error codes when limits are breached: `AUDIT_PAGE_LIMIT_EXCEEDED` when the requested page count exceeds the tier maximum, `AUDIT_ALREADY_RUNNING` when concurrent audit limits are reached, and `AUDIT_CAPACITY_REACHED` when the total capacity unit quota is exhausted.

### Do self-hosted deployments have different audit limits?

Self-hosted OpenSEO installations default to the `"paid"` tier in `resolveAuditLimitTier`, granting access to 10,000 pages per audit and 100,000 capacity units. This behavior ensures that private deployments are not artificially constrained by the free tier limits designed for the hosted SaaS offering.