DeployingBackground Workers

Background Workers

Not every service serves HTTP. Queue consumers, reconcilers, cron-style polling loops, and other background workloads don’t listen on a port — run them in worker mode by setting the container port to 0.

What worker mode changes

When containerPort=0, the platform:

  • Skips PORT injection — the worker doesn’t bind anywhere.
  • Skips the port reachability check — no app port X unreachable log spam and no false-positive “unhealthy” status.
  • Skips EXPOSE in the synthesized Dockerfile.
  • Skips load-balancer route registration — nothing is served. (A generated *.onlizard.com domain may still appear on the service, but it won’t respond.)

Enable worker mode

Three equivalent ways:

# New upload-source worker
lizard up --port 0
 
# Flip an existing service
lizard port 0 --service worker
 
# Via the config:apply path
lizard service set worker --set containerPort=0

Worker mode is a hard switch — a redeploy is needed for the port change to take effect.

Check the current port

lizard port --service worker

Prints the current container port, or worker mode when it’s 0.

When not to use worker mode

Don’t use worker mode for a regular HTTP service that’s just slow to start. Worker mode disables the reachability check entirely, so it will hide “the listener never came up” bugs. If your service is meant to serve traffic, keep a real port and fix the startup instead.

Run a queue consumer

Use the redis-worker example. It moves a job from a pending list to a processing list, stores its uppercase result, and acknowledges the job in a Redis transaction. Keep one replica: startup recovery assumes a single consumer. The example uses Python 3.13 and redis-py 6.4.0.

From the directory containing its Dockerfile:

lizard init --name queue-example
lizard add --service worker
lizard add redis
lizard secrets set REDIS_URL='${{redis.REDIS_URL}}' --service worker
lizard up --service worker --port 0

Sign in with lizard login if a command reports that authentication is required. Use the database’s actual name if it is not redis. Keep the reference quoted. Set it before uploading the application. On macOS with Lizard CLI 0.3.92, prefix lizard up with COPYFILE_DISABLE=1 to exclude AppleDouble metadata.

Check the final deploy event, then check the worker:

lizard port --service worker
lizard logs --service worker --json
lizard ssh --service worker -- python jobs.py enqueue first-job
lizard ssh --service worker -- python jobs.py result first-job

The process prints Queue worker ready. If the log tail is empty, continue with the job check below; the test results record the observed logging limit. The result command waits up to 30 seconds and prints {"id": "first-job", "result": "HELLO"}. A running process alone does not prove it can consume a job. Managed Redis is reached from inside the service; the CLI command does not require your laptop to reach its private address.

Verify a restart

For this test service, restart the worker and submit another job:

lizard restart --service worker
lizard ssh --service worker -- python jobs.py result first-job
lizard ssh --service worker -- python jobs.py enqueue after-restart
lizard ssh --service worker -- python jobs.py result after-restart

Wait for the service to return to running in lizard ps --json if SSH is not yet available. Both results should be HELLO. The first result lives in Redis, so a worker restart does not remove it. On startup the single consumer returns unfinished processing entries to the pending list.

This demo only accepts the JSON jobs created by jobs.py. It does not implement a dead-letter queue, validation for untrusted producers, or exactly-once external side effects. Use an established queue library and idempotent handlers for payments, email or other effects. Redis durability and backup are separate from worker restart behavior; see storage and recovery.

Troubleshooting

SymptomCheck
Missing serviceRun lizard add --service worker after creating the project.
No Queue worker readyCheck the service-scoped REDIS_URL, addon readiness and connection errors. Never print the connection string.
Worker expects an HTTP portSet containerPort=0; changing it on a running service requires a redeploy.
No result after 30 secondsRead worker logs and verify the job ID. This example accepts jobs from its own jobs.py only.
Duplicate effectsA retry can run work again. This example stores a result by job ID; external effects require their own idempotency.

See also

See scenario test results for checked versions, cloud results and remaining limits.

Updated