DeployingBackground Workers

Background Workers

A background worker on Lizard is a service with no HTTP port: a queue consumer, a bot that polls for updates, a reconciler, or a polling loop. Run it in worker mode by setting the container port to 0, for example with lizard up --port 0. The worker gets no PORT, no readiness probe and no public route, so it is never marked unhealthy for not answering HTTP. Pair it with Managed Redis for a job queue. Chat bots are workers too; see the Telegram bot and Discord bot guides.

What worker mode changes

When containerPort=0, the platform:

  • Skips PORT injection: the worker doesn’t bind anywhere.
  • Skips the TCP readiness probe: no false-positive “unhealthy” status. The old pod stops before the new one starts.
  • 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

Redeploy after changing the port to apply worker mode.

Check the current port

lizard port --service worker

The command 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 not detect a server that fails to listen. 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.

Check the final deploy event, then check the worker:

lizard port --service worker
lizard logs --service worker --json

Submit a job from your computer. lizard run --service redis runs a local command with the Redis addon’s connection values, including its public REDIS_URL:

python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
lizard run --service redis -- python jobs.py enqueue first-job
lizard run --service redis -- python jobs.py result first-job

The process prints Queue worker ready, then Processed first-job after the job runs. If the log tail is empty, rely on the job check; 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.

Verify a restart

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

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

If after-restart has no result after 30 seconds, wait for the service to return to running in lizard ps --json and run the result command again. 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