# OpenSEO Audit Limits: How Tiered Restrictions and Capacity Enforcement Work

> Discover OpenSEO audit limits and how tiered restrictions and capacity enforcement ensure fair usage. Learn about free and paid tier limits and runtime validation.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-08-09

---

**OpenSEO enforces specific audit limits through tiered constants (free: 50 pages, paid: 10,000 pages), clamps user requests to safe ranges, and validates capacity at runtime via [`AuditService.ts`](https://github.com/every-app/open-seo/blob/main/AuditService.ts) by checking concurrent audits and total capacity units against subscription tiers.**

OpenSEO uses a three-layer system to control how much work each site audit can perform. Whether you're running the free tier or a paid subscription, the platform guarantees resource fairness through static bounds, capacity estimation, and runtime enforcement. This guide walks through the exact limits, how they're calculated, and where the enforcement logic lives in the `every-app/open-seo` codebase.

---

## Static Audit Limits Defined in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts)

OpenSEO stores its canonical limit values in a single source-of-truth file. These constants determine the floor, ceiling, and default page counts for every audit:

- `MIN_AUDIT_PAGES = 10` — absolute minimum audit size
- `DEFAULT_AUDIT_PAGES = 50` — fallback when no limit specified
- `FREE_MAX_AUDIT_PAGES = 50` — hard cap for free accounts
- `PAID_MAX_AUDIT_PAGES = 10000` — maximum for paid subscriptions

Any component needing limit values imports from this file, ensuring consistency across the codebase. The constants feed into both capacity calculations and runtime enforcement decisions.

---

## Capacity Calculation in [`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)

Before an audit starts, OpenSEO calculates its total resource footprint through two exported functions.

### Clamping Requested Page Counts

The `clampAuditMaxPages` function ensures user-provided values stay withinlegal bounds:

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

```

This guarantees:
- Missing values default to 50 pages
- Requests below 10 pages bump up to the minimum
- Even paid requests cannot exceed 10,000 pages

### Estimating Total Capacity Units

The `getEstimatedAuditCapacity` function combines page counts with Lighthouse overhead:

```ts
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 `lighthouseStrategy` is `"auto"`, OpenSEO reserves 20 capacity units for Lighthouse checks. The `total` field represents the complete resource reservation needed before execution begins.

---

## Runtime Enforcement in [`src/server/features/audit/services/AuditService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/AuditService.ts)

The actual limit checks happen inside `AuditService.startAudit()`, which orchestrates tier resolution, validation, and capacity reservation.

### Step 1: Resolve the User's Tier

OpenSEO determines limit applicability through `resolveAuditLimitTier`:

- **Hosted deployments** — checks the organization's subscription status
- **Self-hosted deployments** — defaults to `"paid"` tier behavior

### Step 2: Validate Page Count Against Tier Maximum

```ts
const limits = AUDIT_LIMITS[input.limitTier];
const maxPages = clampAuditMaxPages(input.maxPages);
if (maxPages > limits.maxPagesPerAudit) {
  throw new AppError("AUDIT_PAGE_LIMIT_EXCEEDED");
}

```

Even after clamping, this secondary check ensures paid users cannot bypass their tier's `maxPagesPerAudit` value. Free tier users hitting this condition receive the `AUDIT_PAGE_LIMIT_EXCEEDED` error.

### Step 3: Reserve Capacity and Check Usage

After inserting the audit row (preventing race conditions), the service validates against running limits:

```ts
const reservation = getEstimatedAuditCapacity({ maxPages, lighthouseStrategy });
// … create audit row …

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");
}

```

**`AuditRepository.getAuditUsageForUser`** aggregates:
- `runningCount` — currently active audits for this user
- `capacityUnits` — sum of all reserved capacity across running and queued audits

---

## Complete Tier Comparison: Free vs. Paid Limits

| Limit Dimension | Free Tier | Paid Tier |
|---------------|-----------|-----------|
| Maximum pages per audit | 50 | 10,000 |
| Concurrent running audits | 1 | Unlimited |
| Maximum capacity units | 2,000 | 100,000 |

The capacity unit system allows OpenSEO to weight audits by their total resource consumption. A 1,000-page audit with Lighthouse enabled consumes 1,020 units (pages + 20 Lighthouse checks), leaving precise headroom calculations possible.

---

## Practical Implementation Examples

### Starting an Audit from an API Handler

```ts
import { AuditService } from "@/server/features/audit/services/AuditService";

async function startUserAudit(req) {
  const { userId, organizationId, projectId, startUrl, maxPages } = req.body;

  const tier = await AuditService.resolveAuditLimitTier(organizationId);
  const result = await AuditService.startAudit({
    actorUserId: userId,
    billingCustomer: { userId, organizationId, ... },
    projectId,
    startUrl,
    maxPages,               // e.g., 200
    lighthouseStrategy: "auto",
    limitTier: tier,
  });
  return result; // { auditId: "..."} or throws AppError if limits exceeded
}

```

### Pre-Validating Capacity for UI Feedback

```ts
import { AUDIT_LIMITS } from "@/shared/audit-limits";

function canRunAudit(tier: "free" | "paid", pagesRequested: number) {
  const limits = AUDIT_LIMITS[tier];
  return pagesRequested <= limits.maxPagesPerAudit;
}

// Returns true for paid plan when pagesRequested ≤ 10,000

```

---

## Summary

- **Static limits** in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts) define bounded ranges for all tiers
- **Capacity calculation** via `clampAuditMaxPages` and `getEstimatedAuditCapacity` prepares precise resource estimates
- **Runtime enforcement** in [`AuditService.ts`](https://github.com/every-app/open-seo/blob/main/AuditService.ts) resolves tiers, validates page counts, and checks concurrent/capacity limits after audit creation
- **Race-condition protection** comes from inserting the audit row before final usage validation
- **Error specificity** — `AUDIT_PAGE_LIMIT_EXCEEDED`, `AUDIT_ALREADY_RUNNING`, `AUDIT_CAPACITY_REACHED` — enables clear user messaging

---

## Frequently Asked Questions

### What happens if I request more pages than my tier allows?

OpenSEO clamps your request to legal bounds first, then throws `AUDIT_PAGE_LIMIT_EXCEEDED` if the clamped value still exceeds your tier's `maxPagesPerAudit`. For free users requesting 100 pages, the clamp produces 50, but since 50 equals the free maximum, the audit proceeds. A paid user with a misconfigured limit of 20,000 pages would see the clamp reduce it to 10,000, then pass validation.

### How does OpenSEO prevent multiple audits from consuming the same capacity?

The enforcement sequence intentionally creates the audit record *before* checking `runningCount` and `capacityUnits`. This serializes concurrent requests through database row creation, eliminating race conditions where two simultaneous requests could both pass usage checks based on stale data.

### Why does Lighthouse "auto" mode add exactly 20 capacity units?

According to the [`audit-capacity.ts`](https://github.com/every-app/open-seo/blob/main/audit-capacity.ts) implementation, the `"auto"` Lighthouse strategy represents a fixed allocation for comprehensive performance testing across key page templates. This predictable overhead simplifies capacity planning compared to dynamic Lighthouse scoring that would require per-page analysis before audit start.

### Can self-hosted deployments modify the free tier limits?

Self-hosted deployments default to paid tier behavior through `resolveAuditLimitTier`, but the `AUDIT_LIMITS` table in [`src/shared/audit-limits.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts) remains configurable at the code level. Operators can adjust `FREE_MAX_AUDIT_PAGES` or introduce additional tiers by extending the limit definitions and updating the tier resolution logic, though this requires source modification and rebuild.