Skip to content

Quick Start

Get from zero to a distributed Actor in about 10 minutes with three steps: your first Actor, a stateful Actor, then the same code across two nodes.


Installation

pip install pulsing

1. Your First Actor (~2 minutes)

Define a class, add @pul.remote, then spawn and call it.

import asyncio
import pulsing as pul

@pul.remote
class Greeter:
    def greet(self, name: str) -> str:
        return f"Hello, {name}!"

async def main():
    await pul.init()
    greeter = await Greeter.spawn()
    print(await greeter.greet("World"))  # Hello, World!
    await pul.shutdown()

asyncio.run(main())

The @pul.remote decorator turns the class into a distributed Actor. spawn() creates an instance; method calls use normal await.


2. Stateful Actor (~3 minutes)

Actors hold state. Here, a counter keeps a value and exposes inc and get.

import asyncio
import pulsing as pul

@pul.remote
class Counter:
    def __init__(self, value: int = 0):
        self.value = value

    def inc(self, n: int = 1) -> int:
        self.value += n
        return self.value

    def get(self) -> int:
        return self.value

async def main():
    await pul.init()
    counter = await Counter.spawn(value=0)
    print(await counter.inc())   # 1
    print(await counter.inc(2))  # 3
    print(await counter.get())   # 3
    await pul.shutdown()

asyncio.run(main())

Same idea: one Actor instance, private state, messages via method calls. No shared memory, no locks.


3. Distributed: Same Code, Two Nodes (~5 minutes)

Run the same Actor type on two processes. Only the initialization changes: bind an address on the first node, join with seeds on the second.

Node 1 (seed):

import asyncio
import pulsing as pul

@pul.remote
class Counter:
    def __init__(self, value: int = 0):
        self.value = value
    def inc(self, n: int = 1) -> int:
        self.value += n
        return self.value

async def main():
    await pul.init(addr="0.0.0.0:8000")
    await Counter.spawn(value=0, name="counter")
    await asyncio.Event().wait()  # keep running

asyncio.run(main())

Node 2 (join cluster, then resolve and call):

import asyncio
import pulsing as pul

@pul.remote
class Counter:
    def __init__(self, value: int = 0):
        self.value = value
    def inc(self, n: int = 1) -> int:
        self.value += n
        return self.value

async def main():
    await pul.init(addr="0.0.0.0:8001", seeds=["127.0.0.1:8000"])
    counter = await Counter.resolve("counter")
    print(await counter.inc(10))  # 10 — same API, remote actor
    await pul.shutdown()

asyncio.run(main())

What changed: init(addr=..., seeds=...) and Counter.resolve("counter") instead of spawn(). The rest of your code stays the same — location transparency.


Next: Choose Your Path

  • LLM Inference Service


    Build a scalable inference backend with streaming and OpenAI-compatible API.

    ~10 minutes

  • Distributed Agents


    Integrate with AutoGen and LangGraph to distribute agents across machines.

    ~10 minutes

  • Use with Ray


    Bridge Ray actors onto the Pulsing network with pul.mount(). Add streaming and discovery to your Ray cluster.

    ~5 minutes


Go Deeper

Goal Link
Named actors and ask vs tell Actor Patterns
Form a cluster (Gossip / Head / Ray) Cluster Setup
Actor basics and patterns Actor Guide
When to use ask / tell / streaming Communication Patterns
Cluster setup and resolve Remote Actors
Operate and inspect Operations