Beyond Stateless Functions
Tasks are stateless: each invocation is independent, and once a task finishes, any local variables it held are gone. This is perfect for pure computations like image processing or data transformation.
But many real systems need persistent state — a model loaded into GPU memory, a counter tracking progress, a cache of recent results, or a connection pool to a database. Recreating this state on every function call would be prohibitively expensive.
Ray Actors solve this. An Actor is a remote class — a Python class that runs as a long-lived process on a worker node, maintaining its own state across method calls.
@ray.remote
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
return self.value
def get(self):
return self.value
# Instantiate the actor — this starts a dedicated process on the cluster
counter = Counter.remote()
# Call methods asynchronously
ref1 = counter.increment.remote() # Returns ObjectRef
ref2 = counter.increment.remote() # Returns ObjectRef
ref3 = counter.increment.remote() # Returns ObjectRef
print(ray.get(ref3)) # 3 — state persisted across calls
Actors as Shared Services
The real power of Actors emerges when multiple tasks need to share state. You can pass an Actor handle to any task, and that task can call methods on it:
@ray.remote
class ModelServer:
def __init__(self, model_path):
import torch
self.model = torch.load(model_path) # Loaded once, stays in memory
def predict(self, input_data):
return self.model(input_data)
# Create one model server
server = ModelServer.remote("/models/resnet50.pt")
# Many tasks can call it concurrently
@ray.remote
def process_request(server_handle, data):
return ray.get(server_handle.predict.remote(data))
futures = [process_request.remote(server, batch) for batch in incoming_batches]
results = ray.get(futures)
This pattern — one Actor serving many Tasks — is the foundation of Ray Serve, Ray's model serving library.
Actor Lifecycle and Fault Tolerance
By default, if an Actor process crashes, Ray will restart it (up to a configurable number of retries). You can control this:
@ray.remote(max_restarts=3, max_task_retries=2)
class ResilientActor:
...
One important nuance: method calls on an Actor are serialized by default. Only one method runs at a time per Actor instance, which prevents race conditions on the internal state. If you need concurrent method execution within a single Actor, Ray supports asyncio-based Actors using async def methods.
A common interview question: "How do Ray Actors differ from microservices?" The key insight is colocation and efficiency. A microservice communicates over HTTP/gRPC with serialization overhead on every call. A Ray Actor communicates through the Plasma Object Store using shared memory when on the same node — orders of magnitude faster. Actors are best for in-process, high-throughput state sharing; microservices are better for cross-team, cross-language service boundaries.