OpenSEO Audit Limits: How Tiered Restrictions and Capacity Enforcement Work

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 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

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

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:

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:

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

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

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:

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

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

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 define bounded ranges for all tiers
  • Capacity calculation via clampAuditMaxPages and getEstimatedAuditCapacity prepares precise resource estimates
  • Runtime enforcement in 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 specificityAUDIT_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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →