Serverless
A worker is a long-lived process. It claims runs from the store, executes them, renews its leases and fires timers, and it drains on SIGTERM (Deploy a worker). Sleeps, signal waits, child runs and schedules only make progress while a worker is running. A function platform that starts a process per request and freezes or stops it between requests cannot do that, so AWS Lambda and Google Cloud Functions are not a worker host.
Where a worker can run
Section titled “Where a worker can run”| Shape | Status |
|---|---|
| Kubernetes Deployment | A recipe: examples/deploy/kubernetes/worker.yaml |
| Docker Compose on one host | A recipe, with Valkey or a SQLite store (Compose) |
| Render background worker | A recipe: Render |
| ECS or Fargate service, Cloud Run service, Railway service | No recipe and not tested. The worker image from Deploy a worker is an ordinary container; what it needs from the platform is below |
What any platform has to give the worker:
- Always running. At least one instance up, with CPU available when no request is arriving. A service that scales to zero, or throttles CPU outside requests, stops claiming runs and stops firing timers. On Cloud Run that means a minimum instance count of one or more and CPU that is not limited to request time; check your platform’s settings for the equivalents.
- Reach the store. Valkey for more than one worker host, SQLite only for a single host (Choosing a store).
- Time to drain. The kill timer must exceed the worker’s shutdown grace: 50 seconds with the defaults (Shutdown).
- A volume for the Cloud spool, if you report to Cloud. Without a persistent volume at
/var/lib/pyrula, unsent Cloud events are lost with the container (details). An ephemeral container filesystem loses the spool. - Probes.
/livezand/readyzonPYRULA_WORKER_HEALTH_PORTfor a platform health check.
Starting runs from a function
Section titled “Starting runs from a function”A function can submit a run and return. The run executes on a worker somewhere else, and the function does not wait for it. There are two ways, and the function needs one of them:
- The client.
WorkflowClientfrompyrula.workflows.syncneeds only the workers’ store, so the function must reach the store (a Valkey the function’s network can connect to). - The run endpoints. If a service mounts
RunRouter, a function canPOST /{name}/runsto it over HTTP, with whatever authentication you configured (HTTP).
A handler using the client. Creating the client outside the handler lets a warm environment reuse
it, but that has not been run on Lambda, and nothing here was run against Valkey: the sync client
owns a loop thread and, on Valkey, sockets that a freeze and thaw can drop. This block runs the handler against a SQLite file on this machine; a real function points
PYRULA_STORE_URL at Valkey, because SQLite serves one host:
import tempfile
from pyrula.workflows import open_storefrom pyrula.workflows.sync import WorkflowClient
# Module level: runs once per execution environment, not once per invocation.store = open_store(default=f"sqlite:///{tempfile.mkdtemp()}/lambda.db")client = WorkflowClient(store)
def handler(event: dict, context: object) -> dict: run = client.submit( "process_document", {"document_id": event["document_id"]}, # A redelivered event gets the first run back. The key lives 24 hours. idempotency_key=f"document:{event['document_id']}", ) return {"run_id": run.run_id}
first = handler({"document_id": "doc_9"}, None)assert handler({"document_id": "doc_9"}, None) == firstclient.close()process_document is the name a worker serves; a function that does not import the workflow
module submits by name and gets no version pin, with a warning
(Starting and talking to runs). To wait for the result, poll
client.handle(name, run_id).status() from another invocation rather than holding the function
open, since the run can outlive any function timeout.
A function is also a fine place for the event-triggered side of the work. See Runs started by events.