# cherry: an actor model game server framework in Go

> cherry is a Go framework for game servers built around the actor model, with one goroutine and a serial mailbox per actor, NATS based cluster discovery, pomelo compatible wire formats, and object pooled messages. It is written for MMO, SLG and RPG back ends, and it publishes no GitHub releases at all, so versions are tracked by commit rather than by tag.

**cherry-game/cherry** — 🍒 Cherry — Go game server framework powered by Actor Model. Serial goroutine per actor, NATS/etcd discovery, pluggable connectors (TCP/WS/HTTP), protobuf & object-pooled messaging,  time wheel, snowflake. Born for MMO/SLG/RPG, scales to millions.

- Repository: https://github.com/cherry-game/cherry
- Website: https://cherry-game.github.io
- Stars: 823 · Forks: 144
- Language: Go
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/cherry-game-cherry

## One goroutine per actor, three serial queues

The core idea is the actor model, and the README states the guarantee plainly: every actor runs in its own goroutine and processes its logic serially. That is the property that matters for a game server, because it means a player entity cannot be modified by two messages at once, and you do not need a lock around your game state.

The cost of that guarantee is that an actor blocks when it blocks, so the framework gives each actor three independent queues, each consumed in FIFO order, and the distinction between them is where the design shows.

```go
type MyActor struct {
    capp.ActorLogger
}

func (p *MyActor) OnInit() {
    p.Local().Register("myHandler", p.handle)
    p.Remote().Register("myRemote", p.remote)
    p.EventRegister("eventName", p.onEvent)
}

func (p *MyActor) handle(session *cproto.Session, req *pb.MyReq) {
    p.Response(session, &pb.MyResp{Value: "ok"})
}
```

The local queue carries client requests to an actor. The remote queue carries messages from another actor, possibly on another node. The event queue carries system events, and it is the one that decouples notification from the sender. Three separate queues rather than one means a broadcast cannot head-of-line block a player action, which is the usual reason a naive single-mailbox actor design falls over under load.

Actors can also create child actors, whose messages are routed by the parent and whose lifetime is shared with it, and timers can be registered directly on an actor so that a scheduled callback runs on the actor's own goroutine rather than on a timer thread.

## A builder chain that assembles and tears down in reverse

Everything is wired through a builder API, and the entry point is a Configure call that takes a profile path, a node identifier, a flag for whether this node accepts client connections, and the cluster mode.

```go
cherry.Configure("etc/profile/dev.json", "game-1", true, cherry.Cluster).
    Register(myComponent).
    SetSerializer(cherryFacade.NewProtobuf()).
    AddActors(myActor).
    Startup()
```

The flag about front end nodes is the one to think about. A node set as a front end node receives client connections, and the framework uses that to decide which connectors to open, so the same binary can run as a gateway, a web node, a centre node, or a game node with only the profile file changing.

The lifecycle is documented as an explicit sequence: Register, then Set, then Init, then OnAfterInit, then the running phase, then OnBeforeStop, then OnStop. Components are stopped in the reverse order of their registration, which is the behaviour you want when component A depends on component B. A graceful shutdown is triggered by the SIGINT, SIGQUIT, and SIGTERM signals, so a container orchestrator stopping a pod gets an orderly drain rather than a kill.

The profile file is the piece with the least documentation. The framework expects a JSON profile at the path you pass, and the discovery mode, among other settings, is chosen inside it, but the repository does not document the profile schema in the README, so the examples and the guide site are where you learn its shape.

## Discovery comes in three modes, and etcd is not in this repository

Member discovery is where a clustered game server lives or dies, and cherry offers three backends selected by profile configuration.

The default mode reads the discovery configuration from the profile file and is intended for single process development and testing. The nats mode uses NATS master and slave heartbeat based discovery for a multi node production environment. The etcd mode uses an etcd lease plus watch, and the README marks it as living in a separate repository rather than in the core module.

That last point is the one to plan around. The etcd component is listed among the open components, together with data-config for design time tables, gin for an HTTP server with middleware, gorm for MySQL, mongo for MongoDB, and cron for scheduled jobs based on robfig/cron. All of them live in the components repository, so adopting etcd discovery means depending on a second module and a second version stream.

The discovery interface is small and gives you what a game server actually needs: a full member map, a lookup by node type, a random member of a type for load spreading, a setting that is broadcast to every node, and callbacks for a member joining or leaving.

```go
discovery := app.Discovery()

all := discovery.Map()
games := discovery.ListByType("game")
member, ok := discovery.Random("gate")
```

Cross node RPC runs over NATS and supports both synchronous and asynchronous calls with a configurable timeout. The random member lookup is the interesting one for a gateway node, since it is how a front end picks a game node without a load balancer in front.

## Two wire formats, one of them for existing pomelo clients

cherry ships two protocol formats, and the choice is a migration decision rather than a technical preference.

The pomelo format is type as one byte, length as three bytes, then the data. The simple format is a four byte id, a four byte data length, then the data. The pomelo format exists so that clients written against the pomelo protocol can connect without changes, which the README backs up with a list of client SDKs for Unity3D, Cocos2d-x, JavaScript, C, iOS, Android and Java, and WeChat, all of them pomelo compatible clients from the pomelo and libpitaya ecosystems.

That compatibility is the single strongest argument for the framework, and it is also the sharpest constraint. If your game client already speaks pomelo, cherry lets you rewrite the server in Go without touching the client, which is a real saving. If your client does not speak pomelo, you are writing a protocol on both sides and the compatibility advantage disappears, leaving you with the simple format and no ecosystem behind it.

The connectors are separate from the protocol. There is a TCP connector for native sockets, a WebSocket connector for browser clients, an HTTP server for REST style APIs, and an HTTP client for outbound calls. The protocol parser and the session management sit above them, with an agent actor per connection, so a connection is itself an actor and benefits from the same serial processing guarantee as everything else.

## Object pooled messages, protobuf by default, JSON when you want it

The messaging layer is built to keep the garbage collector out of the hot path. Messages are taken from a pool rather than allocated, and the README describes the approach as zero allocation message passing built on a sync.Pool with reference counting to reduce GC pressure.

```go
msg := cherryFacade.GetMessage()
msg.Source = "game-1.player-100"
msg.Target = "map-1.aoi"
msg.FuncName = "enter"
msg.Args = myPayload
```

A message carries a source, a target, the function name to invoke, and the arguments, which is a compact remote procedure call envelope rather than a chat protocol. The source and target fields are how an address is resolved, and the example shows a player and a map area as the two ends of a call.

Protobuf is the default serializer and JSON is also supported, chosen through the SetSerializer call in the builder chain. The serializer is per application, not per message, so the format is fixed for the process.

There is an optimisation worth knowing about: messages that stay inside one process are passed without being serialised at all, and only messages that cross a node boundary are encoded and decoded, automatically, through what the README calls ClusterPacket. On a cluster deployment most of your traffic is player to game node and game node to game node, so a meaningful share of it pays the encoding cost, and the local path does not.

Errors are handled with a layered error code scheme, where a session error sits in the 10 range, an RPC error in the 20 range, and an actor error in the 24 range, with OK at zero, and a helper that tells you whether a given code is a failure.

## Twenty-one utility packages, including a time wheel and snowflake IDs

The extend directory carries 21 utility packages grouped by purpose, and this is where cherry saves you writing the small pieces every game server ends up needing.

Time handling has two packages. The time wheel is a hierarchical timing wheel for schedulers, timeouts, and delayed execution, and it is built for the case where you have a large number of pending timers rather than a few. The time package holds helper functions, time offsets, and what the README calls time travel, which is time shifting for testing scenarios.

ID generation has snowflake for distributed unique identifiers, which is the usual answer to needing an ID that is unique across nodes and sortable by time, and nuid for shorter opaque identifiers.

The data packages cover zlib compression, crypto with MD5, CRC32 and Base64, gob and json encodings, and base58. The collections cover map, slice, queue and string helpers, and the infrastructure group has file, http/client, net, and a sync package that contains a rate limiter. The reflection group holds reflect, mapstructure, and a regex helper that caches compiled patterns, which is the one you feel most when a game server parses a lot of chat or command text.

```go
node, _ := cherrySnowflake.NewNode(1)
id := node.Generate()
```

Logging is built on uber-go/zap with structured key value output, multi file output with log rotation, configurable log level and stack trace level, and the option of writing to the console and a file at the same time.

## Two example projects, and no published releases

The repository ships no GitHub releases, which is the first thing to note if you plan to pin a version in a go.mod. The module path is github.com/cherry-game/cherry, the go directive is 1.24.0 with a toolchain of go1.24.2, and the last push was on 2026-10-15. Depending on the master branch is the only documented option.

Two example projects live in a separate examples repository, and they are the fastest way to judge the framework. The first is a minimal single node chat room built on a web client, an HTTP server, a WebSocket connector, and JSON as the wire format, with room creation, message sending, and broadcast. The second is a multi node distributed game built as an H5 client with separate web, gateway, centre, and game nodes, covering a server list, multiple SDK account systems, account registration, login, and character creation. Installation and configuration guidance is hosted on the project documentation site.

The Makefile shows the development tooling. There is a target that installs protoc-gen-go, one that regenerates the protocol buffers in the net/proto directory with protoc, one that runs the tag script, and one that tidies the modules, including the component modules that live in their own repository.

```bash
protoc:
	cd net && \
	cd proto && \
	protoc --go_out=. --go_opt=paths=source_relative proto.proto
```

The licence is MIT, and the README credits two projects as inspiration, pomelo, described there as the first Node.js game server framework, and pitaya, a Go game server framework. Both are also the closest alternatives to cherry, and the difference comes down to what you inherit. A Go server written directly on pitaya gives you a framework without the pomelo client compatibility, and a Node.js server on pomelo gives you the compatibility without Go. cherry is the combination, and the price is that you are committing to a repository with no tagged releases and a profile driven configuration that the README does not fully document.

## Conclusion

cherry fits a team building a Go game back end that already speaks the pomelo protocol, or that wants per-entity serial processing without hand rolling goroutine discipline, and that is prepared to write its own profile file, its own protobuf messages, and its own components. It does not fit a project that needs a published, semver versioned release to pin against, since the repository has no GitHub releases, and it does not fit a team that wants etcd discovery inside the core module, since that piece lives in a separate components repository. Verify first that the profile file your node needs is one you can write, that the pomelo client you intend to use is one of the listed SDKs, and that the discovery mode you pick, default, nats, or etcd, matches the infrastructure you actually run.

## FAQ

### How do I add the cherry game server framework to a Go project?

The documented install is a single go get of the module path github.com/cherry-game/cherry, and the framework needs Go 1.24 or newer. The go.mod of the repository declares go 1.24.0 with a toolchain of go1.24.2.

### What is the difference between the Local, Remote and Event queues in a cherry actor?

Each actor runs in its own goroutine and consumes three independent FIFO queues. The local queue handles player requests from clients, the remote queue carries service to service RPC that may cross nodes, and the event queue carries system events used to decouple notifications.

### Which service discovery backends does cherry support?

Three, selected through the profile configuration. The default mode reads the configuration from the profile file and suits single process development, nats mode uses NATS heartbeat based discovery for multi node production, and etcd mode uses an etcd lease plus watch, with the etcd component published in the separate components repository.

### Can existing pomelo clients connect to a cherry server?

Yes. Cherry implements the pomelo protocol format, and the README lists compatible client SDKs for Unity3D, Cocos2d-x, JavaScript, C, iOS, Android and Java, and WeChat. The framework also offers a simple format with a four byte id and a four byte data length for cases where you write the client yourself.

### Does cherry have published releases or version tags?

The repository has no GitHub releases. The last push was on 2026-10-15, so the project is being worked on, but there is no published version stream to pin a go.mod against, and the profile file that configures a node is not documented in the README.

## Sources

- [cherry-game/cherry on GitHub](https://github.com/cherry-game/cherry)
- [Issues](https://github.com/cherry-game/cherry/issues)
- [License: MIT](https://github.com/cherry-game/cherry/blob/master/LICENSE)
- [Project website](https://cherry-game.github.io)
- [README](https://github.com/cherry-game/cherry/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cherry-game-cherry
