---
title: "Standby mode · Scrape.ly Docs"
description: "Run an Actor as a long-lived HTTP service."
url: https://scrape.ly/docs/sdk-node-standby
markdown: https://scrape.ly/docs/sdk-node-standby.md
---

# 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:

| Setting | Default | Meaning |
| --- | --- | --- |
| `idleTimeoutSecs` | `300` | Extra standby runs with no requests for this long are stopped; the last one is stopped after twice this time |
| `desiredRequestsPerActorRun` | `100` | A new run starts when the average load passes 80% of this |
| `maxRequestsPerActorRun` | none | A run that has served this many requests is replaced |
| `memoryMbytes` | `2048` | Memory per standby run |
| `build` | `latest` | Build used for standby runs. Other users' standby runs use the [User Build](https://scrape.ly/docs/publishing.md#user-build) when it's set. |

Each caller gets their own standby runs; containers aren't shared between users. See the [standby API reference](https://scrape.ly/docs/api-standby.md) 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

| Value | How to read it | Meaning |
| --- | --- | --- |
| Port | `Actor.config.get('containerPort')` | Port your server must listen on: `ACTOR_WEB_SERVER_PORT`, `4321` |
| Container URL | `Actor.config.get('containerUrl')` | `ACTOR_WEB_SERVER_URL`, the URL the platform proxies to this container |
| Standby URL | `Actor.config.get('standbyUrl')` | `ACTOR_STANDBY_URL`, set on standby runs |
| Idle timeout | `process.env.ACTOR_STANDBY_IDLE_TIMEOUT_SECS` | Seconds 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

- [Standby API reference](https://scrape.ly/docs/api-standby.md): invoking a standby Actor over HTTP
- [Environment](https://scrape.ly/docs/sdk-node-environment.md): container metadata and configuration
- [API reference](https://scrape.ly/docs/sdk-node-reference.md)

---

- Previous: [Environment](https://scrape.ly/docs/sdk-node-environment.md)
- Next: [Errors & deployment](https://scrape.ly/docs/sdk-node-deploy.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
