Skip to main content

archipellabs-runtime

An async runtime for distributing work over Redis — Playwright sessions, API calls, SSE streams, timed waits — without losing control of how many run at once.

These tasks spend most of their time waiting (on a network call, a browser, a timer), so you want many of them going at once. Two easy approaches both break:

  • A new task per event: a spike starts thousands at once and falls over — too many browsers, too many open connections.
  • One at a time: safe, but a single slow call holds up everything behind it. All that waiting happens in sequence instead of together.

A service is the middle ground: a fixed budget of workers (max_slots, say 20) sharing the work. Up to 20 run together — enough to overlap the waiting — and never more. Each service has its own budget, so a slow batch of browser sessions cannot hog the workers your API calls need.

Services talk to each other with three verbs and nothing else. No registry, no service discovery, no configuration tying them together: a producer in one process and a consumer in another agree because they share a string.

Distribution name archipellabs-runtime; it imports as runtime.

Install

pip install archipellabs-runtime   # requires Python 3.12+ and a Redis server

Quickstart

import os
from runtime import App, Service

pricing = Service("pricing", max_slots=20)

@pricing.action("pricing.quote")          # one executant, returns a value
async def quote(ctx, params):
    return {"total": round(19.99 * params["qty"], 2)}

@pricing.event("order.placed")            # every subscriber gets a copy
async def note_order(ctx, params):
    print(f"order {params['id']}")

@pricing.every("500ms")                   # runs on a schedule
async def tick(ctx):
    total = await ctx.call("pricing.quote", qty=3)
    print(f"quote: {total}")

app = App(redis=os.environ.get("REDIS_URL", "redis://localhost:6379/0"))
app.include(pricing)
app.start()                               # blocking; logs the topology first

The three verbs

Cardinality Reply Caller
await ctx.call(action, **params) exactly one executant the return value, awaited coupled in time
await ctx.dispatch(action, **params) exactly one executant a task id not coupled
await ctx.emit(event, **params) every subscriber none not coupled

Every message can carry a ttl — a deadline that travels with it, and that any call or dispatch the handler makes inherits and can only narrow — and a delay. Failures cross the wire as typed errors and are re-raised in the caller. Actions can declare a pydantic model for their params and get validation before the handler runs.

await ctx.call("pricing.quote", ttl="2s", qty=3)
await ctx.dispatch("report.build", delay="30s", month="2026-07")
await ctx.emit("order.placed", id="o1")

Prior art

The semantics are lifted from Moleculer — actions, call, a propagated context, typed errors, distributed timeouts. What is not taken is its protocol: no registry, no heartbeats, no discovery, because Redis Streams already do that. dispatch is the one addition, where Moleculer folds decoupled work into a balanced emit.

doc/index.md has the argument; doc/limits.md has what it costs.

Shared resources

A service's lifespan opens what its handlers share — a browser, a DB pool, an API client — once at boot, and closes it on shutdown.

from contextlib import asynccontextmanager

@asynccontextmanager
async def store(config):
    db = await connect(config["dsn"])
    try:
        yield {"db": db}                  # → ctx.resources["db"]
    finally:
        await db.close()

warehouse = Service("warehouse", max_slots=8, lifespan=store)

@warehouse.action("order.fulfil")
async def fulfil(ctx, params):
    await ctx.resources["db"].fulfil(params["id"])

app.include(warehouse, config={"dsn": "postgres://…"})

Documentation

Full documentation is in doc/:

concepts services, the three verbs, choosing between them
messages the envelope, correlation, deadlines, errors, params
internals the keyspace, a message's life, what runs in a process
operations budgets, runtime switches, deployment, App knobs
limits what this deliberately does not do — read before deploying

Runnable examples in examples/:

  • minimal — one service, one action, one producer
  • rpccall, typed errors, params validation, a ttl expiry
  • events — fan-out to independent subscribers
  • jobsdispatch + task_status, a delay, and a runtime switch
  • orders — two processes, lifespans on both sides
  • poisson — a service that consumes and produces
  • playwright — a heavy, isolated cost profile

Upgrading from 0.2? The API is replaced — see CHANGELOG.md.

Deployment

The same App runs in one process, split by role, or scaled to N replicas with no code change. App(namespace=...) isolates environments sharing one Redis; include(enabled=False) leaves a service out of a process entirely, and runtime switches pause one that is mounted.

Development

uv sync --extra dev
uv run python -m pytest                             # auto-detects a backend
RUNTIME_TEST_BACKEND=fake uv run python -m pytest   # CI runs both legs
RUNTIME_TEST_BACKEND=real uv run python -m pytest   # needs a Redis on :6379
uv run python -m pytest --cov=runtime --cov-report=term-missing
uv run mypy && uv run ruff check .

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

archipellabs_runtime-0.3.1.tar.gz (53.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

archipellabs_runtime-0.3.1-py3-none-any.whl (40.8 kB view details)

Uploaded Python 3

File details

Details for the file archipellabs_runtime-0.3.1.tar.gz.

File metadata

  • Download URL: archipellabs_runtime-0.3.1.tar.gz
  • Upload date:
  • Size: 53.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for archipellabs_runtime-0.3.1.tar.gz
Algorithm Hash digest
SHA256 d09f96dddba469563424bdba82baec1c51341808a9a940ef5bb3492771198c2e
MD5 946f9fea441123340ae22a1a5ff8b299
BLAKE2b-256 241d3c45b3ed8c8c4ae9a02456513ed2dc5126f1c79ffcc2e56b8e6636787e6b

See more details on using hashes here.

Provenance

The following attestation bundles were made for archipellabs_runtime-0.3.1.tar.gz:

Publisher: publish.yml on archipellabs/runtime

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file archipellabs_runtime-0.3.1-py3-none-any.whl.

File metadata

File hashes

Hashes for archipellabs_runtime-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8d652d2c8f3ecbcafbeb74e12b53f38891c22fa881857833fbcc7ca7899b4f90
MD5 5c28752bfb221fd9bed0f44896602d4a
BLAKE2b-256 b26dd58bd04033b95670dd2a5ab7e8e28fc652c6c45159f0905fd7dc8a40b5fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for archipellabs_runtime-0.3.1-py3-none-any.whl:

Publisher: publish.yml on archipellabs/runtime

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.2

2 files

This release

0.3.1 This release

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page