Workers, cron jobs and the release command

An app is more than its web server. Declare background workers, scheduled jobs and a release command next to your code, and one deploy ships them all on the same build, with the same environment variables and the same database bindings.

  • web serves traffic. It's the app you already have.
  • Workers run all the time and take no traffic: queue consumers, bots, stream processors.
  • Cron jobs start on a schedule, run to completion and stop.
  • The release command runs once per deploy, after the build and before the new release takes traffic. It's where migrations go.

Workers and cron jobs need a paid plan, and a claimed app (an app deployed by an agent runs them once someone claims it). Forks, Rewind and Replay copies and pull request previews run the web process only. The release command runs on every plan, Free included.

Declaring processes

With a Procfile

If you already have a Procfile, it works as it is:


web: npm start
worker: node worker.js
emails: node email-worker.js
release: npx prisma migrate deploy

web is the start command. release is the release command. Every other line is a worker, named after its line.

With velixir.json

velixir.json at the root of your source says the same and more: replica counts, schedules and timeouts.

{
  "processes": {
    "worker": { "command": "node worker.js", "replicas": 2 },
    "emails": "node email-worker.js"
  },
  "cron": [
    { "name": "nightly-report", "schedule": "0 3 * * *", "command": "node report.js", "timeout": "15m" },
    { "name": "sync", "schedule": "*/15 * * * *", "command": "node sync.js", "timezone": "Europe/London" }
  ],
  "release": { "command": "npx prisma migrate deploy", "timeout": "10m" }
}

  • processes: each key is a worker. The value is a command, or { "command", "replicas" }. A web key overrides the web start command.
  • cron: a list of { "name", "schedule", "command" }, with optional "timezone" and "timeout".
  • release: a command, or { "command", "timeout" }.

You can use both files. When they name the same process, velixir.json wins and the build log says so.

Commands run under sh -c, so pipes, && and $VARIABLES work as they would in a terminal. Names are lowercase letters, digits and hyphens, up to 24 characters.

The build log lists the processes it found. A mistake (a bad schedule, a duplicate name) fails the build in seconds, before the image is built, with a message saying what to fix.

Workers

A worker runs your command in the app's image, around the clock. It gets every environment variable and service binding the web process gets, plus VELIXIR_PROCESS set to its name.

  • Scaling. Set replicas and size on the app's Processes tab, with velixir ps:scale worker=2, or through the API. The replicas in your source is only the starting point: after the first deploy, the dashboard's number wins, so a redeploy never undoes a change you made there.
  • Stopping. Set replicas to 0. The worker is kept but not run or billed.
  • Deploys. Workers move to a new release once its web process is healthy. A release whose web pods never come up leaves the workers on the previous one.
  • Shutdown. On a deploy or scale-down a worker gets SIGTERM and 30 seconds to finish what it's doing before it's stopped. Finish the current job and exit.
  • Billing. Each worker is billed like a web replica: replicas x its plan, by the hour. It runs on the app's plan unless you pick another size for it.
  • Crashes. A worker that exits, runs out of memory or errors is restarted straight away. Each crash is recorded with the last lines it wrote and shown on the app's Overview, and fires an app.crashed webhook (at most one every 10 minutes per process, with a count). One that keeps crashing on start fires app.crash_looping, emails the team's Owners and Admins once, and fires app.recovered when it has run cleanly for 10 minutes. The web process is watched the same way.

Cron jobs

A cron job starts a fresh container on its schedule, runs your command and stops.

  • Schedule. Five fields: minute, hour, day of month, month, day of week. */15 * * * * is every 15 minutes; 0 3 * * 1 is 03:00 every Monday. Schedules are in UTC unless you set "timezone" to a name like Europe/London.
  • No overlaps. A run never starts while the previous one is still going. If it's time and the last run hasn't finished, that start is skipped.
  • Missed starts. A start that can't happen within 5 minutes of its time (the platform was busy, the app was suspended) is skipped, not run hours late.
  • Timeout. A run still going after its timeout (default 1 hour, up to 24 hours) is stopped and counted as failed.
  • No retries. A failed run isn't retried. It's recorded with its exit code and output, and fires a cron.failed webhook.
  • Run now. Start a run outside the schedule from the Processes tab, or with velixir cron run nightly-report.
  • Pause. Pause a schedule from the Processes tab without removing it from your source.
  • History. The Processes tab shows recent runs with their duration, exit code and the last lines of output. Live output is on the Logs tab while a run is going.
  • Billing. Per second of runtime, at the hourly rate of the plan it runs on. A job that runs for 20 seconds every 15 minutes costs a fraction of an always-on worker.

The release command

The release command runs once per deploy, on the newly built image, after the build and before any traffic moves. It runs once, not once per replica, so two replicas never race to apply the same migration.

  • If it fails, the deploy stops. Nothing about the running app changes: the previous release keeps serving, and the deploy shows the exit code and the last lines of output.
  • Timeout. Default 10 minutes, up to 30.
  • Environment. The same variables and bindings as the app, so DATABASE_URL points where it always does.
  • Once per release, on the live slot. A staging slot shares the live slot's database, so deploying a branch to staging doesn't migrate it: the command runs when that release is swapped in. A pull request preview with a database of its own migrates that database.
  • Never on a rollback. Rolling back, swapping back or redeploying a release whose command already succeeded doesn't run it again, so an emergency rollback never waits on a migration. It doesn't run your migrations backwards either: the older code runs against the schema you have.

Write migrations the old release can live with

While the release command runs, the previous release is still serving traffic against the same database. Make each migration safe for the code that's already running:

  1. Expand. Add the new column or table. Don't remove or rename anything the running code reads.
  2. Deploy the code that uses it.
  3. Contract. In a later deploy, remove what nothing reads any more.

Renaming a column is an add, a copy, a deploy, and then a drop.

Limits

Workers per app 10
Cron jobs per app 20
Replicas per worker 10
Cron timeout 1 minute to 24 hours (default 1 hour)
Release command timeout 1 minute to 30 minutes (default 10 minutes)
Command length 4,000 characters

From the CLI and API

velixir ps                         # list processes
velixir ps:scale worker=2          # set a worker's replicas (0 stops it)
velixir cron run nightly-report    # start a cron job now
velixir cron runs nightly-report   # recent runs with output

The API has the same under /api/v1/apps/{id}/processes. See the API reference.