Add free-tier limits to your Apify actor

Push free users toward your paid plan without breaking the experience. This guide gives you a drop-in helper that caps results per run, throttles runs per day, and enforces a cooldown — while paid users and your own account bypass everything.

Use with an AI agent

Open this guide as a pre-filled prompt — or copy it for Claude Code, Cursor, Codex, or any other coding agent.

Why do this at all?

Apify lets you ship an actor with a free trial, but the SDK doesn't enforce trial limits for you. If you want free users to get a real taste — without footing the bill for unlimited runs — you need three caps:

  • Results per run. A hard ceiling on the output dataset size.
  • Runs per day. Stops free users from chaining N small runs to get an effectively-unlimited dataset.
  • Cooldown between runs. Smooths out load and discourages scripted abuse.

Paid users (anyone on an Apify paid plan) are detected via the APIFY_USER_IS_PAYING environment variable that the Apify platform injects into every run. See how to tell if an Apify user is paying for the full rundown.

The helper

Drop this file alongside your main entry point. It uses a named key-value store called trial-stateso the counters persist across runs for the same user but don't pollute your default storage tab.

import { Actor } from 'apify';

// Tune these for your actor.
const TRIAL_MAX_RESULTS = 25;                    // results per run
const TRIAL_MAX_DAILY_RUNS = 3;                  // runs per UTC day
const TRIAL_MIN_INTERVAL_MS = 30 * 60 * 1000;    // 30-minute cooldown

const STATE_STORE = 'trial-state';
const STATE_KEY = 'state';

/**
 * Returns the max results to deliver on this run.
 *   0  = unlimited (paid user, local dev, or your own account)
 *   25 = free-tier cap is in effect
 *
 * Calls Actor.fail() and exits if the user hit the daily or cooldown cap.
 */
export async function enforceTrialLimits() {
  // 1. Local dev — never limited
  if (!Actor.isAtHome()) return 0;

  // 2. Test runs from the Apify Console — never limited
  if (process.env.APIFY_META_ORIGIN === 'TEST') return 0;

  // 3. Paid Apify plan — unlimited
  if (process.env.APIFY_USER_IS_PAYING === '1') return 0;

  // 4. Your own account — replace with your APIFY_USER_ID
  if (process.env.APIFY_USER_ID === 'YOUR_APIFY_USER_ID') return 0;

  const store = await Actor.openKeyValueStore(STATE_STORE);
  const state = (await store.getValue(STATE_KEY)) ?? {};

  const now = Date.now();
  const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD (UTC)
  const dailyRunCount = state.date === today ? state.dailyRunCount ?? 0 : 0;
  const lastRunAt = state.lastRunAt ?? 0;

  // Cooldown
  const elapsed = now - lastRunAt;
  if (lastRunAt && elapsed < TRIAL_MIN_INTERVAL_MS) {
    const waitMin = Math.ceil((TRIAL_MIN_INTERVAL_MS - elapsed) / 60_000);
    await Actor.fail(
      `Free tier: please wait ${waitMin} more minute(s) before your next run, or upgrade to a paid plan.`,
    );
    return 0;
  }

  // Daily cap
  if (dailyRunCount >= TRIAL_MAX_DAILY_RUNS) {
    await Actor.fail(
      `Free tier: ${TRIAL_MAX_DAILY_RUNS} runs per day — try again tomorrow, or upgrade to a paid plan.`,
    );
    return 0;
  }

  // Record this run *before* doing any real work.
  await store.setValue(STATE_KEY, {
    date: today,
    dailyRunCount: dailyRunCount + 1,
    lastRunAt: now,
  });

  Actor.log.info(
    `Free tier: run ${dailyRunCount + 1}/${TRIAL_MAX_DAILY_RUNS} today, capped at ${TRIAL_MAX_RESULTS} results.`,
  );
  return TRIAL_MAX_RESULTS;
}

The return value is the contract: 0 means “no cap — use whatever the user asked for,” and a positive number is the free-tier ceiling you should clamp to.

Wire it into your actor

Call enforceTrialLimits() right afterActor.init() and use its return value as the effective max:

import { Actor } from 'apify';
import { enforceTrialLimits } from './trial-limits.js';

await Actor.init();

const input = (await Actor.getInput()) ?? {};

// Returns 0 for paid users (no cap) or 25 for free users.
const trialCap = await enforceTrialLimits();
const effectiveMax = trialCap > 0 ? trialCap : input.maxItems ?? 0;

// ... rest of your actor, using effectiveMax in place of input.maxItems

await Actor.exit();

If your actor takes a list of input URLs and produces one result per URL, also slice the list so you don't pay upstream for data you'll never deliver:

// In a "1 URL → 1 result" mode, also trim the input list so you don't pay
// upstream costs for results you'll never deliver.
const startUrls = trialCap > 0 ? input.startUrls.slice(0, trialCap) : input.startUrls;

For discovery-style actors that paginate until they hit a target, you don't need this — just push to the dataset until you reach effectiveMax.

Bypass for yourself

Apify injects APIFY_USER_ID on every run. Hardcoding your own ID lets you test the actor as if you were a regular user without burning your daily allowance. Find your ID at console.apify.com → Account → Integrations.

Better yet, lift the ID into an environment variable on your actor build so you don't leak it in source control.

The helper also bypasses limits when APIFY_META_ORIGIN is TEST — the origin Apify sets for runs launched from the Test tab in the Console. That way trying your actor from its listing never burns a daily slot or trips the cooldown. Apify reports several other origins too (WEB, API, SCHEDULER, CLI, STANDBY, …); TESTis the only one you'd typically want to exempt.

Gotchas worth knowing

  • Failed runs still count as runs. When the cooldown or daily cap kicks in, Apify still records a run — but it costs almost nothing because you exit before any scraping happens.
  • Crashes burn a daily slot. The helper saves lastRunAt and increments the counter beforedoing work. If you'd rather only count successful runs, move the setValue call to the end of your actor — but be aware users can then crash on purpose to dodge the cap.
  • The day rolls over in UTC. A user in PT gets their reset at 4pm/5pm local. That's fine for most actors; if you care, swap toISOString().slice(0, 10) for a timezone-aware format.
  • State is per actor + user. The trial-statestore lives inside the run's storage scope, so every user gets an independent counter automatically. No shared global state to synchronize.
  • Make the limits visible to free users. Mention them in your README and your actor description. Surprised users leave bad reviews; informed users upgrade.

Where to go next

Once your limits are in place, run the Apify Pricing Calculator to translate “25 free results, then $X for 1,000” into the bundle pricing string that goes on your actor listing.

Spotted a bug, or want a guide on something else?

support@mail.apifyhub.com