How the Audit Capacity Tier System Limits Crawling in OpenSEO
The audit capacity tier system limits crawling by enforcing plan-specific caps on pages per audit, total capacity units consumed, and concurrent running audits through maxPagesPerAudit, maxCapacityUnits, and maxRunningAudits constraints.
OpenSEO's site audit workflow needs guardrails to prevent resource exhaustion. The platform implements a three-tier capacity system—free, paid, and self-hosted—that clamps crawl scope before any HTTP request leaves the server. This article explains exactly where those limits live in the codebase and how they gate the crawling pipeline.
Where Capacity Limits Are Defined
The AUDIT_LIMITS Record
All tier configurations reside 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). The system defines a Record<AuditLimitTier, TierLimits> mapping each tier to its three enforcement levers:
export const AUDIT_LIMITS: Record<
AuditLimitTier,
{ maxPagesPerAudit: number; maxCapacityUnits: number; maxRunningAudits: number }
> = {
free: {
maxPagesPerAudit: FREE_MAX_AUDIT_PAGES,
maxCapacityUnits: 2_000,
maxRunningAudits: 1
},
paid: {
maxPagesPerAudit: PAID_MAX_AUDIT_PAGES,
maxCapacityUnits: 100_000,
maxRunningAudits: Number.POSITIVE_INFINITY
},
self_hosted: {
maxPagesPerAudit: PAID_MAX_AUDIT_PAGES,
maxCapacityUnits: Number.POSITIVE_INFINITY,
maxRunningAudits: Number.POSITIVE_INFINITY
},
};
The numeric constants FREE_MAX_AUDIT_PAGES (50) and PAID_MAX_AUDIT_PAGES (10,000) are declared in [src/shared/audit-limits.ts](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts), making them importable by both server and client code.
Three Enforcement Points in the Crawl Pipeline
1. Page Count Clamping Before Estimation
The clampAuditMaxPages function prevents oversized requests from ever reaching the crawler. It enforces a hard floor (MIN_AUDIT_PAGES) and ceiling (PAID_MAX_AUDIT_PAGES), with paid/self-hosted tiers later restricted by their maxPagesPerAudit lookup:
export function clampAuditMaxPages(maxPages?: number) {
return Math.min(
Math.max(maxPages ?? DEFAULT_AUDIT_PAGES, MIN_AUDIT_PAGES),
PAID_MAX_AUDIT_PAGES,
);
}
Source: [audit-capacity.ts](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) lines 42-46
For free tiers, this clamping effectively forces maxPagesPerAudit to 50, since PAID_MAX_AUDIT_PAGES (10,000) is reduced to FREE_MAX_AUDIT_PAGES during tier validation elsewhere in the pipeline.
2. Capacity Unit Budget Calculation
Before spawning crawl workers, the system calculates total capacity units—a composite metric combining page fetches plus Lighthouse analysis 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
};
}
Source: [audit-capacity.ts](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) lines 49-62
A free-tier user requesting 200 pages receives a capacity estimate of 70 units (50 clamped pages + 20 Lighthouse checks), well under their 2,000-unit budget—but their page cap has already been violated, triggering an earlier rejection.
3. Runtime Enforcement in AuditService
The AuditService.start method (line 55 and subsequent validation logic) performs the final gate check:
- Looks up
AUDIT_LIMITS[input.limitTier] - Compares
estimatedCapacity.totalagainstmaxCapacityUnits - Compares current running audit count against
maxRunningAudits
If either threshold is exceeded, the promise rejects before any crawl state is initialized. This protects Worker compute and database connection pools from runaway audits.
What Users See When Limits Hit
The client surfaces tier-aware error messaging using constants imported from the shared limits module:
AUDIT_PAGE_LIMIT_EXCEEDED:
`Free plan audits are limited to ${FREE_MAX_AUDIT_PAGES} pages. Upgrade to run larger audits.`
Source: [src/client/lib/error-messages.ts](https://github.com/every-app/open-seo/blob/main/src/client/lib/error-messages.ts) line 16
This single source of truth prevents drift between enforcement logic and user communication.
Practical Capacity Checking Examples
Estimating a Free-Tier Audit
import { getEstimatedAuditCapacity } from "@/server/features/audit/services/audit-capacity";
// User requests 200 pages; clamping enforces the free tier ceiling
const estimate = getEstimatedAuditCapacity({ maxPages: 200 });
console.log(estimate.pagesTotal); // → 50
console.log(estimate.total); // → 70 (50 + 20 Lighthouse)
Validating Before Starting
import { AUDIT_LIMITS, getEstimatedAuditCapacity } from "@/server/features/audit/services/audit-capacity";
function canStartAudit(
tier: "free" | "paid" | "self_hosted",
requestedPages: number
): boolean {
const limits = AUDIT_LIMITS[tier];
const { total } = getEstimatedAuditCapacity({ maxPages: requestedPages });
return total <= limits.maxCapacityUnits;
}
console.log(canStartAudit("paid", 9_500)); // true
console.log(canStartAudit("free", 100)); // true (capacity OK, but page cap will block)
Key Implementation Files
Summary
- Audit capacity tiers (free, paid, self-hosted) govern crawling through three numeric levers:
maxPagesPerAudit,maxCapacityUnits, andmaxRunningAudits - Page clamping occurs via
clampAuditMaxPagesbefore any capacity calculation, with paid/self-hosted tiers capped at 10,000 pages and free tiers at 50 - Capacity units combine page fetches plus Lighthouse overhead, enforced against tier budgets in
AuditService.start - Self-hosted deployments inherit paid page limits but remove capacity unit and concurrency caps entirely
- All constants live in
audit-limits.ts; all enforcement logic inaudit-capacity.ts; runtime validation inAuditService.ts
Frequently Asked Questions
How do I increase my audit page limit beyond 50?
Upgrade from the free tier to paid or self-hosted. According to the source code in [audit-capacity.ts](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts), paid tiers raise maxPagesPerAudit to 10,000 pages and maxCapacityUnits to 100,000 units, while self-hosted removes the capacity unit ceiling entirely.
What happens if I request more pages than my tier allows?
The clampAuditMaxPages function silently reduces your request to the tier maximum during capacity estimation, but AuditService will reject the audit with an AUDIT_PAGE_LIMIT_EXCEEDED error before crawling begins. The error message references FREE_MAX_AUDIT_PAGES from [audit-limits.ts](https://github.com/every-app/open-seo/blob/main/src/shared/audit-limits.ts).
Why does the free tier allow only one concurrent audit?
The maxRunningAudits: 1 constraint in AUDIT_LIMITS.free protects shared compute resources. Paid tiers set this to Number.POSITIVE_INFINITY, and self-hosted deployments inherit the same unlimited behavior, as implemented in [audit-capacity.ts](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) lines 14-18.
How is capacity consumption estimated before crawling starts?
The getEstimatedAuditCapacity function in [audit-capacity.ts](https://github.com/every-app/open-seo/blob/main/src/server/features/audit/services/audit-capacity.ts) sums clamped page counts with Lighthouse strategy overhead—20 units for "auto" mode. This estimate is validated against maxCapacityUnits without performing any actual HTTP requests, preventing wasted resources on doomed audits.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →