Open-source project
anthdm/hollywood avatar
anthdm/hollywood

anthdm/hollywood: an actor engine for Go where the local and remote paths differ

Blazingly fast and light-weight Actor engine written in Golang

2,341 stars180 forksGoMIT

At a glance

What is it?
A Go actor library built on dRPC with protobuf messages, cluster discovery over multicast DNS or Consul, and a benchmark that reports 3.5 million messages a second. The mechanism is simple to read and the interesting parts are the type rules that change when an actor goes remote, and a delivery guarantee that rests on a buffer.
Who is it for?
Adopt hollywood if you are building a Go service where per-actor isolation and restart semantics matter more than the simplicity of a plain function call, because the PID, the lifecycle messages and the buffer-on-failure behaviour are all there and the race-detector test suite suggests they were built carefully.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 118 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Engine, receiver, PID: the whole model in three calls

The install is one command, and it is the only place the README states a requirement:

bash
go get github.com/anthdm/hollywood/...

The Go version given there is 1.21, which the module file contradicts, as the last section of this piece explains. The quickstart itself is worth reading in full because it is the entire public surface. You create an engine with actor.NewEngine(actor.NewEngineConfig()), which returns an engine and an error, and the engine is the thing that spawns actors, sends messages and owns their lifecycle. Actors are referred to as receivers, because they implement actor.Receiver, and you spawn one by passing a constructor function and a string ID: engine.Spawn(newHelloer, "hello") returns a PID, a pointer identifying that one actor instance on this engine. IDs must be unique; PIDs are what you use locally, and IDs are what you use when talking to a remote system. The actor itself is an ordinary struct with a single Receive method taking an actor.Context. Inside it you switch on the message type, and three of those cases are the lifecycle the engine sends you rather than you: actor.Initialized, actor.Started and actor.Stopped. Everything else is yours. There is no interface to implement beyond Receive, no registration step, and no reflection in the hot path, which is the point of the design.

Local messages can be anything, remote messages must be protobuf pointers

This is the single most consequential rule in the README and it is stated almost as an aside. Any type can be a message on a local engine. To cross the wire a message must be serializable, and for protobuf to serialize it the message must be a pointer. On top of that, the README tells you to start with local examples because the compiler can infer the types, and that when you run remotely you have to provide protobuf definitions for the compiler. The mechanism is visible in the toolchain: the Makefile has a proto target that runs protoc with a vtprotobuf plugin, and go.mod pulls in planetscale/vtprotobuf, which is the generator behind the claim of optimized protobufs without reflection. So the code generation is a real part of the workflow, not an afterthought. The consequence for a project is a migration cliff. A system built entirely on local actors can use small value types, unexported fields and in-process shortcuts, and it will compile and run. Move one actor to a second node and every message on that path has to become a generated protobuf pointer. Nothing warns you at compile time beyond the wire requirement, and the README does not describe a compatibility check or a way to keep both shapes in one codebase. Plan the message types as protobuf from the first commit, or accept the rewrite later.

The headline says ten million a second, the benchmark says three and a half

The README claims the engine can handle ten million messages in under a second, and then publishes a benchmark that does not quite say that. Running make bench produces output showing ten engines spawned with two thousand actors each, twenty concurrent senders over ten seconds, and a final rate of 3,511,664 messages per second, with 35,116,641 sent and 35,116,641 received and zero dead letters. Both numbers are from the same README, and they are not the same claim. At the rate the benchmark reports, ten million messages take roughly 2.8 seconds rather than under one. That gap is not a scandal, because the two figures may well be measuring different things, with the headline presumably counting something other than end-to-end handled messages, and the README does not reconcile them. It matters anyway, because a headline figure is what ends up in your architecture document. The benchmark itself is a genuinely useful artefact: a reproducible target, a documented topology, and an explicit dead-letter count, which is the number you would want to see non-zero if delivery guarantees were broken. Note also that everything measured is local, single process, in-memory. The README publishes no figure for cluster throughput, and a number three orders of magnitude away is exactly what cross-node delivery costs, so treat the local figure as a floor on the cost of a message rather than as a capacity plan.

Guaranteed delivery is a buffer, and the documentation does not bound it

The feature list promises guaranteed message delivery on actor failure and attributes it to a buffer mechanism, which is the honest way to put it and also tells you what the cost is. When an actor dies, messages addressed to it are not dropped; they are held, and the expectation is that they reach the actor when it comes back. The examples directory backs this up with a restarts folder and a persistance folder, the latter misspelled in the tree, which together describe the recovery story. The trade-off is inherent to the mechanism. A buffer that guarantees delivery cannot also have a fixed capacity, because the moment it is full, either the guarantee lapses or the process runs out of memory. The README does not say which happens, does not give a buffer size, and does not document an overflow policy, so this is the question to ask before you rely on it. Two smaller details sit alongside. The dead letter count in the benchmark implies there is a dead letter path, which means messages can end up somewhere other than their destination and you need to know what drains it. And the engine imports DataDog's gostackparse, which is a stack trace parser, suggesting actor panics are captured with their stack rather than only logged, so the failure is diagnosable once it has happened.

Two ways to find the cluster, one of them very heavy

Cluster support is described as writing distributed self-discovering actors, and the implementation offers two discovery mechanisms. The examples folder contains an mdns directory, and go.mod includes grandcat/zeroconf, which is a multicast DNS service discovery library for the local network. It also includes hashicorp/consul/api, which is the client library for a service mesh or key-value service. So you can discover peers on a LAN with zeroconf or register with a Consul cluster, and the choice is between zero infrastructure and some. The dependency footprint is worth noticing. The Consul client drags in serf, the memberlist gossip layer, hclog for logging, a DNS library, and a set of HashiCorp and mitchellh transitive dependencies, all of which are present in go.mod whether or not you use them. If your deployment is a handful of hosts on one network, pulling the Consul client to get multicast discovery is a cost paid in binary size and in transitive updates. If your deployment is a real cluster across machines, it is the right dependency. Notably, the README does not document either option; the mechanisms are visible in the examples directory and the module file rather than described, so the discovery decision is one you would make by reading code.

Race detector, a twenty second timeout, and twelve example binaries

The Makefile is where the engineering discipline shows. The test target depends on build, and build compiles twelve example programs into bin/, so the test run cannot start until every example in the repository compiles. The test command itself is go test ./... -count=1 --race --timeout=20s, and each of those flags is a statement. The race detector on a concurrency library is not optional diligence, it is the minimum. Disabling the test cache with -count=1 avoids reporting stale passes. The twenty second timeout is the interesting one, because it means a test that deadlocks fails the suite rather than hanging it, which is the failure mode a message-passing engine is most exposed to. There is also a bench-profile target that runs a single named benchmark with both a CPU and a memory profile, so the performance claims have a repeatable profiling path rather than a one-off. Two supporting signals: the repository has a Go Report Card badge and a build workflow badge, and the top level contains two hand-rolled concurrency packages, ringbuffer and safemap, rather than depending on an external collection library. Those two directories are the engine's primitives, and knowing they are local to the project is relevant if you ever need to reason about what happens under contention.

The install instruction and the module file disagree about Go

The installation section is one command, and the requirement stated next to it is Golang 1.21. The module file says something different: go 1.22.12 with a toolchain directive of go1.24.0. The module declaration is what the toolchain actually enforces, so the README is out of date by a minor version and a toolchain line, and someone on Go 1.21 following the documentation will get a toolchain download or an error rather than a working build. It is a small defect and it is the kind that costs a newcomer half an hour, which is why it is worth checking before you install anything. The release picture has the same shape. There are three tags in recent history: v1.0.3 from 2025-01-03, v1.0.4 from 2025-02-17, and v1.0.5 from 2025-02-24, whose title is a phrase about cluster improvements rather than a version note. The last push was on 2026-06-06, so there are three months of commits past that tag and about sixteen since it was published. The library is MIT licensed, which removes the usual question for a component this central. The remaining question is the one the tags raise: this is a project that shipped three 1.0 patches in two months and then went quiet on releases while continuing to commit, and either it is stable or it is under-maintained, and the repository does not say which.

Editorial conclusion

Adopt hollywood if you are building a Go service where per-actor isolation and restart semantics matter more than the simplicity of a plain function call, because the PID, the lifecycle messages and the buffer-on-failure behaviour are all there and the race-detector test suite suggests they were built carefully. Do not adopt it if your messages are not already protobuf-shaped, since anything that crosses a node must be a serializable pointer and local convenience disappears the moment you distribute. Verify three things before you commit. Read the benchmark rather than the headline, because the README claims ten million messages in under a second while its own output reports 3,511,664 messages per second. Check the Go version, since the install section says 1.21 while go.mod declares 1.22.12 with a go1.24.0 toolchain. And decide how much buffering you can afford, because guaranteed delivery on actor failure is implemented with a buffer and the repository does not say what bounds it.

Frequently asked questions

How do I install anthdm/hollywood?

With go get github.com/anthdm/hollywood/... The README states Golang 1.21 is required, though the module file declares go 1.22.12 with a go1.24.0 toolchain, so the module declaration is the stricter requirement.

What throughput does the hollywood benchmark report?

The published benchmark output shows ten engines with two thousand actors each, twenty concurrent senders over ten seconds, and 3,511,664 messages per second, with 35,116,641 messages sent and received and zero dead letters. The README headline separately claims ten million messages in under a second.

What message types can I send between actors in hollywood?

Any type works locally. To cross the wire a message must be serializable, and for protobuf to serialize it the message must be a pointer. The README recommends starting with local examples because the compiler infers types locally, and notes that remote actors need protobuf definitions supplied to the compiler.

How does hollywood discover other cluster members?

Two mechanisms are present: multicast DNS through the grandcat/zeroconf dependency, with an mdns example in the examples directory, and HashiCorp Consul through hashicorp/consul/api. The README describes cluster support as self-discovering actors but does not document either option in detail.

How are the hollywood tests run?

Through the Makefile, where the test target depends on a build target that compiles twelve example programs first, then runs go test with the count cache disabled, the race detector enabled and a twenty second timeout.

Official sources

  1. anthdm/hollywood on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/anthdm-hollywood.svg)](https://hysenlabs.com/projects/anthdm-hollywood)