Skip to content
JavaScript SDK

Standby mode

Run an Actor as a long-lived HTTP service.

Standby mode keeps an Actor running as an HTTP service instead of executing once and exiting. A standby Actor starts, listens on a port, and answers requests until the platform scales it down, so callers get a response without waiting for a container to boot.

Use standby when an Actor is called often with small inputs and latency matters. Use a normal run for batch crawls, where startup time is small next to the work itself.

Enabling standby

Set "usesStandbyMode": true in your Actor config (for example .actor/actor.json) and build the Actor, or turn standby on in the Actor's settings. Standby settings and their defaults:

SettingDefaultMeaning
idleTimeoutSecs300Extra standby runs with no requests for this long are stopped; the last one is stopped after twice this time
desiredRequestsPerActorRun100A new run starts when the average load passes 80% of this
maxRequestsPerActorRunnoneA run that has served this many requests is replaced
memoryMbytes2048Memory per standby run
buildlatestBuild used for standby runs. Other users' standby runs use the User Build when it's set.

Each caller gets their own standby runs; containers aren't shared between users. See the standby API reference for the settings API and how callers send requests.

Detecting standby

.ts
import { Actor } from 'scrapely';

await Actor.init();

if (Actor.isStandby()) {
    startServer();
} else {
    await runBatch();
    await Actor.exit();
}

Actor.isStandby() is true when ACTOR_META_ORIGIN (or APIFY_META_ORIGIN) is STANDBY, so one entrypoint can serve both modes.

Container port and URLs

ValueHow to read itMeaning
PortActor.config.get('containerPort')Port your server must listen on: ACTOR_WEB_SERVER_PORT, 4321
Container URLActor.config.get('containerUrl')ACTOR_WEB_SERVER_URL, the URL the platform proxies to this container
Standby URLActor.config.get('standbyUrl')ACTOR_STANDBY_URL, set on standby runs
Idle timeoutprocess.env.ACTOR_STANDBY_IDLE_TIMEOUT_SECSSeconds without requests before the run is stopped

The SDK doesn't start a server for you. It only reports the port. Listen on it and bind to 0.0.0.0; a server bound to 127.0.0.1 isn't reachable from outside the container.

The platform considers a new standby run ready once GET / on the port returns any HTTP response, and waits up to 5 minutes for that. Answer / quickly, even before heavy setup finishes.

A standby Actor

.ts
import { createServer } from 'node:http';
import { Actor, log } from 'scrapely';

await Actor.init();

const port = Actor.config.get('containerPort');

const server = createServer(async (req, res) => {
    const { pathname, searchParams } = new URL(req.url ?? '/', 'http://localhost');

    if (pathname === '/') {
        res.writeHead(200).end('ok');
        return;
    }

    try {
        const url = searchParams.get('url');
        if (!url) {
            res.writeHead(400, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'url query parameter is required' }));
            return;
        }

        const response = await fetch(url);
        const html = await response.text();
        const title = /<title>(.*?)<\/title>/is.exec(html)?.[1]?.trim() ?? null;

        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ url, title }));
    } catch (error) {
        res.writeHead(500, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: (error as Error).message }));
    }
});

server.listen(port, '0.0.0.0', () => {
    log.info(`Listening on ${port}`);
});

This example never calls Actor.exit(). Exiting ends the run and defeats the purpose of standby; let the platform manage the lifetime.

With Express

.ts
import express from 'express';
import { Actor } from 'scrapely';

await Actor.init();

const app = express();
app.use(express.json());

app.get('/', (_req, res) => res.send('ok'));

app.post('/scrape', async (req, res) => {
    const { url } = req.body;
    if (!url) {
        return res.status(400).json({ error: 'url is required' });
    }

    const response = await fetch(url);
    res.json({ url, status: response.status });
});

app.listen(Actor.config.get('containerPort'), '0.0.0.0');

Add express to your package.json dependencies.

Writing to storage from standby

A standby run can use datasets and key-value stores, but every request shares the run's same default storages for the whole lifetime of the container. A default dataset collects rows from many requests.

When each request needs its own output, return the result in the HTTP response, or open a named storage per request:

.ts
const dataset = await Actor.openDataset(`results-${requestId}`);
await dataset.pushData(items);

Handling shutdown

When the platform scales a standby run down (idle timeout or maxRequestsPerActorRun), it marks the run ABORTING and stops the container. The process receives SIGTERM and is killed about 5 seconds later; no aborting event is sent. The SDK's own SIGTERM handler flushes storage and charges within those 5 seconds and exits.

Stop accepting connections as soon as SIGTERM arrives:

.ts
process.on('SIGTERM', () => {
    server.close(() => log.info('Server closed'));
});

A graceful abort through the API sends aborting first and allows 30 seconds; with the default graceful shutdown, the SDK then calls Actor.exit().

Sharing state across requests

Module-level variables persist between requests in the same container, which makes them useful as a cache. But a container can be replaced at any time and several containers may serve traffic at once. Treat in-memory state as a cache, never as the source of truth. Anything that must survive belongs in a key-value store, for example through Actor.useState().

Checklist

  • Set usesStandbyMode or enable standby in the Actor settings
  • Check Actor.isStandby() to choose behavior at startup
  • Listen on Actor.config.get('containerPort'), bound to 0.0.0.0
  • Answer GET / quickly so the run becomes ready
  • Don't call Actor.exit() on the request path
  • Return request-specific results in the response
  • Close the server on SIGTERM; you have about 5 seconds
  • Keep per-request state out of module scope

What's next