How to Avoid Apify Actor Maintenance Flags
If you publish actors on the Apify Store, you have probably seen it — that dreaded "Under maintenance" badge that appears on your actor's listing. It signals to every potential user that your actor is broken, unreliable, or abandoned.
The maintenance flag kills your actor's visibility in search results, destroys user trust, and tanks your revenue if you are using Pay Per Event pricing. The worst part? Most maintenance flags are entirely preventable.
We manage over 250 actors on the Apify Store. At our peak, 12 actors were simultaneously under maintenance — a painful week that cost us hundreds of dollars in lost revenue and weeks of rebuild trust. Since implementing the prevention strategies in this guide, we have kept maintenance flags to zero for over 3 months straight.
This guide covers what triggers the maintenance flag, how to prevent it, and how to recover quickly when things go wrong.
What Triggers the Maintenance Flag?
Apify monitors actor health automatically. When your actor consistently fails, the platform steps in. Here are the specific triggers, ranked by how often we see them.
1. Failing on Default Inputs
This is the number one trigger — responsible for roughly 60% of all maintenance flags we have experienced. Every public actor must run successfully with its default input configuration. Apify periodically runs actors with their default inputs as a health check. If that run fails, your actor gets flagged.
The most common scenarios:
- Your default input references a URL that has changed or gone offline
- A required API key is not set in the default input (and your actor crashes instead of handling it gracefully)
- Your default input schema does not match what your code actually expects
- You updated your code to expect new input fields but forgot to update the defaults
- The default URL returns different content than expected (e.g., a website redesign)
2. Consistent Run Failures
If a high percentage of user-initiated runs fail, Apify notices. A few failures are normal — users pass bad inputs, websites change their markup, rate limits kick in. But if your failure rate climbs above approximately 50% over a sustained period (usually a few days), expect a flag.
From our experience: an actor needs roughly 5+ consecutive failures or a failure rate above 50% over recent runs to trigger this. Single transient failures are ignored.
3. Schema Validation Issues
A malformed INPUT_SCHEMA.json can cause problems before your actor even runs. If the schema declares required fields but your default input does not include them, or if the schema has structural errors, the Apify platform may not be able to present your actor's input form correctly.
4. Timeout Without Output
Actors that consume their entire timeout without producing any output or status updates look broken to the platform. Even if your actor is doing work, if it never pushes results or logs progress, it can appear hung. This is especially common with actors that process large datasets without emitting intermediate results.
5. Build Failures
If your latest build fails to compile or start, the actor cannot run at all. This often happens when dependencies break (a major version bump in a library), Dockerfile syntax errors, or missing files in your build context.
Prevention Strategy 1: Bulletproof Your Input Schema
Your INPUT_SCHEMA.json is the contract between your actor and its users. Errors here cascade into everything else.
Common Schema Mistakes
Here is a schema that will eventually cause a maintenance flag:
{
"title": "My Actor Input",
"type": "object",
"properties": {
"url": {
"title": "URL",
"type": "string",
"description": "URL to scrape"
}
},
"required": ["url"]
}
This schema marks url as required, but there is no default value. Apify's health check cannot run your actor without manual input — guaranteed maintenance flag.
Fix: Always provide sensible defaults for required fields, or make fields optional with fallback logic in your code:
{
"title": "My Actor Input",
"type": "object",
"properties": {
"url": {
"title": "URL",
"type": "string",
"description": "URL to scrape",
"default": "https://example.com",
"prefill": "https://example.com",
"editor": "textfield"
},
"maxResults": {
"title": "Max Results",
"type": "integer",
"description": "Maximum number of results to return",
"default": 10,
"prefill": 10,
"minimum": 1,
"maximum": 1000
},
"outputFormat": {
"title": "Output Format",
"type": "string",
"description": "Format of the output data",
"default": "json",
"prefill": "json",
"enum": ["json", "csv", "xml"],
"editor": "select"
}
},
"required": ["url"]
}
The Schema Validation Checklist
Run through this checklist for every INPUT_SCHEMA.json:
- Every
requiredfield has adefaultvalue - Every
defaultvalue matches the declaredtype(no"10"for anintegerfield) - Every
defaultvalue is in theenumlist if one exists (case-sensitive) prefillvalues matchdefaultvaluesminimum/maximumconstraints do not exclude thedefaultvalue- Array fields have a default of
[]or a valid array, notnull - Boolean fields have an explicit
defaultoftrueorfalse
Automated schema validation catches these issues before they reach production. The Schema Validator checks your schema against Apify's requirements and flags missing defaults, type mismatches, and structural errors before you push.
Schema Validation in Code
You can also validate inputs at runtime as a safety net:
import { Actor, log } from 'apify';
import Ajv from 'ajv';
import inputSchema from './INPUT_SCHEMA.json' assert { type: 'json' };
await Actor.main(async () => {
const input = await Actor.getInput() || {};
// Validate input against schema
const ajv = new Ajv({ useDefaults: true, coerceTypes: true });
const validate = ajv.compile(inputSchema);
if (!validate(input)) {
log.warning('Input validation issues:', validate.errors);
// Apply defaults manually rather than crashing
}
// Now input has defaults applied via Ajv's useDefaults option
const url = input.url || 'https://example.com';
const maxResults = input.maxResults || 10;
log.info(`Processing ${url} with max ${maxResults} results`);
// ... rest of your actor logic
});
Prevention Strategy 2: Test with Default Inputs Before Every Push
This sounds obvious, but most maintenance flags come from developers who push code changes without re-testing the default input flow.
Before every apify push:
# Run your actor locally with default inputs
apify run
# Check the output — did it produce results?
# Check the exit code — did it exit cleanly?
If your actor depends on external websites, your default input should target something stable. Do not use a random blog post as your default URL — use a well-known site that is unlikely to restructure. Even better, test against multiple default scenarios.
Choosing Stable Default URLs
From our experience managing 250+ actors, here is what works and what does not for default URLs:
Stable choices:
https://example.com— maintained by IANA, never changes- Major tech company homepages (google.com, github.com) — rarely restructure
- Wikipedia articles — extremely stable URL structure
- Your own website or a page you control
Unstable choices (avoid these):
- Blog posts — get deleted, moved, or restructured
- Small business websites — change frequently or go offline
- Social media profiles — privacy settings change, accounts get suspended
- Government pages — redesigns happen without warning
Automated Pre-Push Testing
A proper Test Runner automates the entire testing workflow. It runs your actor with its default inputs, verifies the output structure matches expectations, and flags regressions before they hit the store. Adding this to your pre-push workflow eliminates the most common cause of maintenance flags.
Here is a minimal pre-push check script:
import { readFileSync } from 'fs';
// Step 1: Load and validate schema
const schema = JSON.parse(readFileSync('INPUT_SCHEMA.json', 'utf8'));
const required = schema.required || [];
const errors = [];
for (const field of required) {
const prop = schema.properties?.[field];
if (!prop) {
errors.push(`Required field "${field}" not found in properties`);
continue;
}
if (prop.default === undefined && prop.prefill === undefined) {
errors.push(`Required field "${field}" has no default or prefill value`);
}
if (prop.type === 'integer' && typeof prop.default === 'string') {
errors.push(`Field "${field}" type is integer but default is string`);
}
if (prop.enum && prop.default && !prop.enum.includes(prop.default)) {
errors.push(`Field "${field}" default "${prop.default}" not in enum`);
}
}
// Step 2: Verify package.json
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
if (!pkg.scripts?.start) {
errors.push('No start script in package.json');
}
// Step 3: Report
if (errors.length > 0) {
console.error('PRE-PUSH VALIDATION FAILED:');
errors.forEach(e => console.error(` - ${e}`));
process.exit(1);
} else {
console.log('All pre-push checks passed');
}
Prevention Strategy 3: Graceful Error Handling
Your actor will encounter errors. Websites go down, APIs rate-limit you, inputs contain garbage. The difference between a healthy actor and a flagged one is how it handles these situations.
The Anti-Pattern: Crashing on Unexpected Input
import { Actor } from 'apify';
await Actor.main(async () => {
const input = await Actor.getInput();
const response = await fetch(input.url); // Crashes if url is undefined
const data = await response.json(); // Crashes if response isn't JSON
await Actor.pushData(data.results); // Crashes if data.results is undefined
});
This actor has three crash points in three lines. Any of these will produce a FAILED run status, and enough of them will trigger maintenance.
The Correct Pattern: Defensive Coding
import { Actor, log } from 'apify';
await Actor.main(async () => {
const input = await Actor.getInput() || {};
// Validate input with fallbacks
const url = input.url?.trim();
if (!url) {
log.warning('No URL provided, using default');
input.url = 'https://example.com';
}
// Network request with error handling
let response;
try {
response = await fetch(input.url, {
signal: AbortSignal.timeout(30000) // 30 second timeout
});
} catch (error) {
log.error(`Failed to fetch ${input.url}: ${error.message}`);
await Actor.pushData([{
url: input.url,
error: error.message,
timestamp: new Date().toISOString()
}]);
return; // Exit gracefully, don't crash
}
// HTTP error handling
if (!response.ok) {
log.error(`HTTP ${response.status} for ${input.url}`);
await Actor.pushData([{
url: input.url,
error: `HTTP ${response.status}`,
timestamp: new Date().toISOString()
}]);
return;
}
// JSON parsing with error handling
let data;
try {
data = await response.json();
} catch {
log.error('Response was not valid JSON');
await Actor.pushData([{
url: input.url,
error: 'Invalid JSON response',
timestamp: new Date().toISOString()
}]);
return;
}
// Safe data access with fallbacks
const results = Array.isArray(data.results) ? data.results : [];
log.info(`Pushing ${results.length} results`);
if (results.length > 0) {
await Actor.pushData(results);
} else {
log.warning('No results found for the given input');
}
});
The Key Principles
- Never assume input fields exist — validate and provide fallbacks
- Catch network errors — external services fail constantly
- Set timeouts on every external call — do not let a hung connection consume your entire run budget
- Log meaningfully — users need to understand what went wrong
- Exit cleanly — an actor that returns zero results is better than one that crashes
- Push error data rather than throwing — a SUCCEEDED run with an error field is infinitely better than a FAILED run
Handling Rate Limits
Rate limiting from external APIs is one of the sneakiest causes of maintenance flags because it is intermittent:
async function fetchWithBackoff(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const response = await fetch(url);
if (response.status === 429) {
const waitTime = Math.pow(2, attempt) * 1000;
log.warning(`Rate limited, waiting ${waitTime}ms (attempt ${attempt}/${maxRetries})`);
await new Promise(r => setTimeout(r, waitTime));
continue;
}
return response;
}
throw new Error(`Failed after ${maxRetries} retries due to rate limiting`);
}
Prevention Strategy 4: Emit Progress Updates
For long-running actors, emit status updates so the platform knows you are alive:
const totalItems = urls.length;
for (let i = 0; i < totalItems; i++) {
await processUrl(urls[i]);
// Update status every 10 items
if (i % 10 === 0) {
const progress = Math.round((i / totalItems) * 100);
await Actor.setStatusMessage(`Processing: ${progress}% (${i}/${totalItems})`);
log.info(`Progress: ${i}/${totalItems} URLs processed`);
}
}
await Actor.setStatusMessage(`Complete: ${totalItems} URLs processed`);
This prevents timeout-based false positives and gives users visibility into what is happening. It also helps you debug — if an actor gets stuck, the status message tells you exactly where.
Prevention Strategy 5: Pin Your Dependencies
Dependency drift is a silent killer. An unpinned ^3.0.0 in your package.json might resolve to 3.5.2 today and 3.6.0 next week — and that minor version bump could introduce a breaking change.
{
"dependencies": {
"apify": "3.1.10",
"cheerio": "1.0.0-rc.12",
"playwright": "1.40.1"
}
}
Pin exact versions for critical dependencies. Use npm shrinkwrap or commit your package-lock.json to ensure reproducible builds. We learned this the hard way when a Cheerio update changed the .text() method behavior and broke 15 scrapers simultaneously.
Recovering from a Maintenance Flag
If your actor gets flagged, here is the exact recovery process we follow:
Step 1: Diagnose the Root Cause
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const runs = await client.actor('your-actor-id').runs().list({ limit: 10 });
// Find the failing runs
const failures = runs.items.filter(r => r.status === 'FAILED');
for (const run of failures) {
const log = await client.run(run.id).log().get();
console.log(`Run ${run.id} failed at ${run.finishedAt}:`);
console.log(log.slice(-500)); // Last 500 chars of log
}
Step 2: Fix the Root Cause
Do not just patch the symptom. If the default URL broke, do not just change the URL — add resilience so it handles URL failures gracefully.
Step 3: Test Locally
apify run
Confirm the fix works with default inputs. Then test with empty input, malformed input, and any edge case the failure logs revealed.
Step 4: Push and Verify
Push the fix and immediately trigger a manual test run through the Apify Console or API. The maintenance flag should clear within hours once your actor passes health checks again.
Step 5: Prevent Recurrence
Add the failure scenario to your test suite. If a website change broke your actor, add a more resilient selector strategy. If an API changed, add version checking. Every maintenance flag should result in a new automated test.
Automated Monitoring at Scale
Manual vigilance does not scale, especially if you manage more than a handful of actors. Here is what to automate:
- Schema validation on every push — catch input schema errors before they reach production
- Default input test runs — run after every deployment to verify the health check will pass
- Error rate monitoring — alert when failure rates spike above your threshold
- Output validation — verify that runs produce the expected data structure
- Dependency auditing — check for breaking changes in upstream packages weekly
We run our fleet monitoring daily across all 250+ actors. It has caught 47 potential maintenance flags before they happened — issues that would have taken days to discover manually and cost significant revenue.
ApifyForge provides a suite of tools specifically designed for this: the Schema Validator catches input schema issues, the Test Runner verifies default input behavior, and the dashboard gives you a fleet-wide view of actor health.
The Bottom Line
The maintenance flag exists to protect Apify Store users from broken actors. Work with it, not against it. Every prevention measure you implement makes your actors more reliable for users and more profitable for you.
The actors that dominate the Apify Store are not necessarily the most sophisticated — they are the ones that always work. Reliability beats features every single time. Invest in prevention now, and you will never have to explain a maintenance flag to a paying user again.
Related resources:
- Schema Validator — catch schema issues before they cause flags
- Test Runner — automated pre-publish testing
- Scraping Compliance — check legal risk before scraping
- Compare Compliance Tools — sanctions and screening actor comparison