Skip to content

Running in production

The executor, workers and egress: how to run them.

What runs where

Functions add up to four kinds of process. All are the same backd binary (the executor’s image adds Deno), each in its own role:

ProcessCommandNeeded forHow many
APIbackd servecalling functions over HTTPas many as you like, all with the same config
Executorbackd executorrunning any functionone or more, reachable only from backd and workers
Workerbackd worker, or backd serve --with-workerasync jobs and cron schedulesone or more; each job and each scheduled run is done by exactly one
Egressbackd egressfunctions that declare networkone or more; the executor’s only way out

The smallest useful deployment is backd serve --with-worker plus an executor (add egress once a function declares network). The production reference runs them as separate services with the executor on a network of its own, and the project template’s compose.yaml runs the same set for local development.

Sizing

  • Executor: EXECUTOR_MAX_PROCESSES functions run at once (default 64), each using its memory plus about 130 MiB; beyond that calls answer 503 (a busy executor leaves async jobs queued for retry). Set the container’s memory limit for that many.
  • Workers: WORKER_CONCURRENCY jobs per worker process (default 10), and the executor’s process limit above bounds the total. Long jobs hold a slot for their whole run: size for the jobs that run at the same time, not the ones per day.
  • backd: each function’s concurrency (default 10) and the realm’s functions.max_concurrency cap sync calls per instance; per-caller rate_limit is shared across instances.

What to watch

  • Startup warnings name functions that declare a secret with no value, functions callable anonymously with no rate_limit, scheduled functions when serve runs without --with-worker, and an executor that isn’t configured or doesn’t answer.
  • "msg":"function called" log lines carry each call’s function, status and duration: alert on the rate of statuses other than ok.
  • Queue health: backd functions jobs --realm <realm> --status queued (or the admin API) that keeps growing, or an old queued job, means no worker is keeping up.
  • Schedules: the newest run of every scheduled function is recent and ok (see Checking that a run happened).
  • Retention: functions.log_retention and functions.job_retention in realm.yaml decide how long history, logs and finished jobs are kept.

Troubleshooting

SymptomLikely causeFix
503 unavailable on every function callThe executor isn’t configured or isn’t reachableSet BACKD_EXECUTOR_URL and BACKD_EXECUTOR_TOKEN (the same on both sides); check the network between them
503 in bursts, Retry-After: 1The executor is at EXECUTOR_MAX_PROCESSES, or a webhook function is at its concurrency limitRaise the limit or add executors; make slow work async
429 too_many_requestsA function’s concurrency on that instance, or the caller’s rate_limitRetry after Retry-After; tune the limits
500 secret_missingThe function declares a secret that isn’t setbackd secret set --realm … --name … (--database … for a database secret)
500 function_failedThe function threw something other than ctx.error, crashed, or exceeded memorybackd functions logs; catch client errors and use relay
504 function_timeoutIt ran longer than its timeoutRaise timeout (up to 60 s), or make it async
backd refuses to start: functions are not builtMissing, stale or modified bundlesbackd functions build, then redeploy (Building)
--with-worker requires BACKD_EXECUTOR_URLA worker needs the executorSet BACKD_EXECUTOR_URL/BACKD_EXECUTOR_TOKEN
A job stays queuedNo worker is runningWhen a job doesn’t finish
A scheduled run is missingNo worker was up within five minutes of its timeScheduled functions
Calls to an outside host failThe host isn’t in network, or backd egress isn’t reachableOutbound network

Running the executor

Functions run in the executor, backd executor: a supervisor that starts one Deno process per call and stops it at its deadline. It runs in its own container, from the backd-executor image (the official Deno image plus backd; build it from docker/executor/Dockerfile). It never reads CONFIG_DIR or MongoDB: it fetches each function’s bundle from backd by its hash, and functions reach data only through backd.

backd talks to the executor, and the executor and functions talk back to backd on an internal listener that must never be published:

VariableOnMeaning
BACKD_EXECUTOR_URLbackd serveWhere the executor listens, e.g. http://executor:9100. Without it, calls to functions answer 503 (startup warns)
BACKD_EXECUTOR_TOKENbothA shared secret, 32 characters or more: backd calls the executor with it, and the executor fetches bundles with it
BACKD_INTERNAL_ADDRbackd serveThe internal listener’s address (default :8081), served when functions exist and the executor is configured
BACKD_CALLBACK_URLbackd serveThe internal listener as the executor and functions reach it, e.g. http://backd:8081
BACKD_URLbackd serve, backd workerbackd’s public address, e.g. https://api.example.com: the base of the links in emails. A realm’s email.public_url overrides it; startup fails when a realm sends email and neither is set
BACKD_CALLBACK_KEYbackd serveA secret, 32 characters or more, signing the short-lived credentials functions call back with. Every backd instance of a deployment needs the same one; the executor never gets it
EXECUTOR_ADDRexecutorListen address (default :9100)
EXECUTOR_DIRexecutorWritable directory for the bundle cache (the image uses /tmp/backd-executor: mount a tmpfs)
EXECUTOR_MAX_PROCESSESexecutorFunctions running at once (default 64); beyond it, calls answer 503
EXECUTOR_PROXYexecutorbackd egress’s base URL, e.g. http://egress:3128. Without it, function processes get no outbound network beyond --allow-net on this host
BACKD_EGRESS_KEYexecutor, egressA shared secret, 32 characters or more, required with EXECUTOR_PROXY: the executor signs each invocation’s egress credentials with it, egress verifies them
EGRESS_ADDRegressListen address (default :3128)
EGRESS_ALLOW_PRIVATEegressComma-separated host:port exceptions to the private-address block; only backd’s internal listener belongs here
WORKER_CONCURRENCYbackd worker, backd serve --with-workerAsync jobs one worker process runs at once (default 10)

The internal listener serves only bundles (to the executor) and the data routes, for functions calling back with their short-lived credentials; sessions and API keys aren’t accepted there, and those credentials aren’t accepted on the public listener.

Run the executor as a non-root user, with a read-only root filesystem, no capabilities and a tmpfs for EXECUTOR_DIR. Size its memory limit for EXECUTOR_MAX_PROCESSES functions (each uses its memory plus about 130 MiB), and its process limit for about 5 per running function.

Running the worker

backd worker claims and runs async jobs — same binary and image as serve, a different role. It needs BACKD_EXECUTOR_URL and BACKD_EXECUTOR_TOKEN (it calls the executor directly, the same way serve does for sync calls) and BACKD_CALLBACK_URL/BACKD_CALLBACK_KEY (functions it runs still reach data through backd’s API, via whichever instance’s internal listener BACKD_CALLBACK_URL points at — the worker doesn’t need to run one itself). It also needs BACKD_SECRETS_KEY if any async function declares secrets, and reads CONFIG_DIR/MONGO_URI like serve.

Run it as its own deployment (scale workers independently of API instances) with backd worker, or fold it into an API instance with backd serve --with-worker for a smaller deployment — both drive the same claim loop, so several workers (or several --with-worker instances) safely share one queue: each job is claimed by exactly one of them at a time.

Scheduled functions (cron) run only while at least one worker is running.

Edit this page · Last updated 08 Oct 2026