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 sizeDEFAULT_AUDIT_PAGES = 50— fallback when no limit specifiedFREE_MAX_AUDIT_PAGES = 50— hard cap for free accountsPAID_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 usercapacityUnits— 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.tsdefine bounded ranges for all tiers - Capacity calculation via
clampAuditMaxPagesandgetEstimatedAuditCapacityprepares precise resource estimates - Runtime enforcement in
AuditService.tsresolves 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 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →