Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

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.

ShapeStatus
Kubernetes DeploymentA recipe: examples/deploy/kubernetes/worker.yaml
Docker Compose on one hostA recipe, with Valkey or a SQLite store (Compose)
Render background workerA recipe: Render
ECS or Fargate service, Cloud Run service, Railway serviceNo 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. /livez and /readyz on PYRULA_WORKER_HEALTH_PORT for a platform health check.

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. WorkflowClient from pyrula.workflows.sync needs 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 can POST /{name}/runs to 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_store
from 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) == first
client.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.

View this page as Markdown