Library / SDK
tidwall/uhaha avatar
tidwall/uhaha

uhaha turns a Raft application into a set of commands, and the Redis protocol is the interface

High Availability Raft Framework for Go

655 stars39 forksGoMIT

At a glance

What is it?
tidwall/uhaha is a Go framework for building highly available Raft based data applications, built on hashicorp/raft with the Redis protocol served by default so existing clients work unchanged. It is MIT licensed with no tagged releases, it defaults to binding only the loopback address, and it succeeds the tidwall/finn project with a redesigned API.
Who is it for?
Use uhaha if you want a replicated state machine in Go without writing the Raft plumbing yourself and you are willing to expose a Redis style command interface to your clients. Do not adopt it if you need a documented, versioned API with tagged releases, since the repository publishes none, or if your clients need a protocol other than Redis without building a custom service.
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 72 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 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The application is a config struct and a command handler

Uhaha is a framework for building highly available Raft based data applications in Go, and the shape of the API is the first thing to understand.

You do not write a server, a transport, a snapshot format or a membership protocol. You fill in a uhaha.Config, register the commands that mutate state, and hand the whole thing to uhaha.Main. The command handler receives a Machine, and from it asks for the current data, mutates it, and returns a value to the caller.

The example in the documentation is a monotonically increasing ticket service:

go
package main

import "github.com/tidwall/uhaha"

type data struct {
	Ticket int64
}

func main() {
	// Set up a uhaha configuration
	var conf uhaha.Config
	
	// Give the application a name. All servers in the cluster should use the
	// same name.
	conf.Name = "ticket"
	
	// Set the initial data. This is state of the data when first server in the 
	// cluster starts for the first time ever.
	conf.InitialData = new(data)

	// Since we are not holding onto much data we can used the built-in JSON
	// snapshot system. You just need to make sure all the important fields in
	// the data are exportable (capitalized) to JSON. In this case there is
	// only the one field "Ticket".
	conf.UseJSONSnapshots = true
	
	// Add a command that will change the value of a Ticket. 
	conf.AddWriteCommand("ticket", cmdTICKET)

	// Finally, hand off all processing to uhaha.
	uhaha.Main(conf)
}

// TICKET
// help: returns a new ticket that has a value that is at least one greater
// than the previous TICKET call.
func cmdTICKET(m uhaha.Machine, args []string) (interface{}, error) {
	// The the current data from the machine
	data := m.Data().(*data)

	// Increment the ticket
	data.Ticket++

	// Return the new ticket to caller
	return data.Ticket, nil
}

Four decisions in that snippet do most of the work. The cluster name goes in conf.Name and every server in the cluster must use the same one. conf.InitialData is the state when the very first server in the cluster starts for the first time, which is the bootstrap question that Raft applications usually get wrong. conf.UseJSONSnapshots turns on the built in JSON snapshot system, which the documentation qualifies with a real constraint: the important fields in the data must be exportable to JSON, meaning they have to be exported fields, and in this example there is only one, Ticket.

And conf.AddWriteCommand binds the name ticket to the handler, which is what marks the command as a write that must go through the Raft log rather than being answered locally.

The API surface is deliberately tiny, which is the point of the project. The rest of the machinery, including the write ahead log and the membership commands, is provided.

Redis is the wire protocol, so existing clients work

The interface an application exposes is the Redis protocol, and the documentation states that it is supported by default, so most Redis client libraries will work with Uhaha.

That is a bigger design decision than it first appears. It means the client side of a replicated service needs no new library, no new connection handling and no new error model. A client that speaks Redis can talk to a Raft replicated key value store, a timeseries store or a ticket service with the same code, and the same redis-cli can inspect all three.

The framework also lets you build custom services such as HTTP and gRPC, so the Redis protocol is the default rather than the only option. But the default is the interesting part, because it is the difference between a framework you can point an existing client at and one that needs an adapter.

The built in command set covers cluster management. There are commands to show the application version, to show information about the state machine, and to query Raft state: RAFT LEADER for the address of the current leader, RAFT INFO for information about the raft server and cluster, and RAFT SERVER LIST for all servers in the cluster. Membership is mutable at runtime with RAFT SERVER ADD, REMOVE, PROMOTE and DEMOTE, the last two moving a server between non-voter and voter status. Snapshots are driven by RAFT SNAPSHOT NOW, with LIST, FILE and READ for inspecting and downloading them, and READ accepting an optional range.

The client side commands are the familiar ones: QUIT, PING, ECHO and AUTH.

For an operator, that means the health of a cluster is inspectable from the same connection the application uses, which is more than most Raft libraries offer out of the box.

Building and starting a three node cluster

The ticket example is the fastest way to see the whole thing work, and the documentation walks through it end to end.

Building uses the source file from the examples directory:

code
go build -o ticket examples/ticket/main.go

The documentation says it is ideal to have three, five or seven nodes in your cluster, which is the usual Raft guidance about odd majorities. The first node is created with a name and an address:

code
./ticket -n 1 -a :11001

That creates a node named 1 and binds the address to port 11001. Two more nodes then join it:

code
./ticket -n 2 -a :11002 -j :11001
./ticket -n 3 -a :11003 -j :11001

The -j flag points the new node at an existing member, here node 1, and the result is a fault tolerant three node cluster. The naming and port conventions in the documentation are worth copying exactly in your own deployment, because the -j argument is how a new node discovers the cluster it is joining, and a node that cannot reach that address will never join.

Once the cluster is up, the client side is the Redis protocol. You connect with redis-cli, telnet or netcat, and the documentation uses redis-cli:

code
redis-cli -p 11001

Then a TICKET command returns the first ticket:

code
> TICKET
"1"

and every subsequent call returns a larger value, which the documentation guarantees for the lifetime of the cluster. Note the port: the client connects to 11001, and the documentation says to connect to the leader, which will probably be the first node you created, though a leader change moves that address.

Joining is a flag, and the leader is not where you think

Two operational facts follow from the way the cluster is assembled, and both catch people out.

The first is that joining is a startup argument rather than a runtime protocol you drive. A node is given the address of an existing member and bootstraps from it. There is no separate membership tool, no configuration file listing peers and no discovery service. The trade is simplicity: you must know the address of a live member when you start a new node, and a node started with no reachable peer has nothing to join.

The second is that the client has to find the leader. The documentation is explicit that you connect to the leader and that this will probably be the first node you created. That is true right after the cluster forms, and it is not a guarantee. If node 1 fails, the cluster elects a new leader among nodes 2 and 3, and the address your client was using is now a follower.

The built in commands exist for exactly this. RAFT LEADER shows the address of the current raft leader and RAFT SERVER LIST shows all servers in the cluster, so a client or a supervisor can discover where to send writes. Whether your client implements that discovery is a decision you have to make, and the documentation does not describe a client side failover recipe, so treat leader discovery as your own responsibility.

For a stateful service, that distinction is the difference between a cluster and a single node with a raft log attached. Replication only buys availability if clients can follow the leader.

The default binds to loopback, and opening it up is your decision

The network section states a default that is easy to miss and important to keep: by default a single Uhaha instance is bound to the local 127.0.0.1 IP address. The consequence is that nothing outside that machine, including other servers in the cluster or machines on the same local network, can communicate with the instance.

That default is safe, and it is also a trap for a clustered deployment. A node started with an address like :11001 binds to all interfaces, while a node started with 127.0.0.1 binds to nothing but loopback. If your peers cannot reach a node, the cluster never forms, and the symptom is a node that starts and then sits alone.

The documentation shows the three server setup on a local 10.0.0.0 network, each node given a distinct address and each joining node 1:

code
./ticket -n 1 -a 10.0.0.1:11001

and the second and third with the join flag pointing at 10.0.0.1:11001.

The documentation is careful about what this buys you. It says this may be enough for applications that only require a network security policy, and that basically any server on the local network can access the cluster. That is the point to internalise: exposing the address moves the trust boundary from one machine to the whole network segment, and the framework adds nothing on top of that.

The second lock is the auth password, described as a secret auth that the cluster and client will need to communicate with each other. It is passed with the --auth flag, and the documentation says all the servers must be started with the same auth, and that the client needs the same auth too. The client side is the AUTH command. TLS is listed as a supported feature in the features list, with its own section referenced from the table of contents.

Deterministic time, recalculable randomness and JSON snapshots

Three of the framework's features exist because a replicated state machine has requirements that an ordinary service does not.

The first is deterministic monotonic time that does not drift and stays in sync with the internet. In a Raft log, every replica must apply the same entries and end up with the same state, so a command handler cannot simply read the wall clock: two replicas running the same entry at different moments would compute different results. The dependency list shows how that is handled, with tidwall/rtime as the time source.

The second is recalculable random numbers, listed alongside deterministic time in the feature list. The same problem, in a different form: a random value produced during a state transition has to be reproducible on replay, so randomness has to be derived from something the log already contains rather than from a source that changes.

The third is snapshots, and the project describes them as simpler than in the project it replaces. The example uses the built in JSON snapshot system with conf.UseJSONSnapshots set to true, and the constraint is that the fields you care about must be exportable to JSON. That is a real modelling requirement rather than a limitation of the snapshot code: unexported state cannot be captured, so a data type with an unexported field will not round trip through a snapshot.

The dependency list also shows a choice worth noting. There is no bbolt, LevelDB or Bolt store in the main require block for storage. Instead there are two snapshot storage backends, hashicorp/raft-boltdb/v2 and tidwall/raft-leveldb, and the write ahead log is tidwall/wal. The gomodule/redigo dependency provides the Redis protocol server, and tidwall/redcon is the Redis server itself.

MIT terms, no releases, and a lineage back to Finn

Uhaha is described as an upgrade to the tidwall/finn project, with an updated API, better security features covering TLS and auth passwords, customizable services, deterministic time, recalculable random numbers, simpler snapshots, a smaller network footprint, and more.

That lineage explains the style. The project is by the same author as redcon, wal, and the other tidwall building blocks it depends on, so the framework is assembled from parts the author also maintains, which is why the dependency list is full of tidwall modules rather than only well known third party libraries.

The module path is github.com/tidwall/uhaha and go.mod declares go 1.25.0, which sets the toolchain floor. The main dependency block pins hashicorp/raft at v1.7.3, and the storage backends are hashicorp/raft-boltdb/v2 and tidwall/raft-leveldb, with tidwall/wal v1.2.1 for the log.

The examples directory holds three complete applications: a key-value DB, a timeseries DB, and the ticket service. Each is a small program in its own directory, which makes them usable as starting points rather than only as documentation.

Two gaps are worth stating. The repository has no GitHub releases, so there is no tagged version to depend on, and the main branch is the only reference. The repository is not archived and the last push was on 2026-08-12, so the code is still being worked on.

For an adopter, the practical risk is not that the project is dead, it is that a dependency without tags gives you no upgrade path other than tracking a branch. Vendoring uhaha into your own build, or pinning to a specific commit, is a reasonable response to that.

Editorial conclusion

Use uhaha if you want a replicated state machine in Go without writing the Raft plumbing yourself and you are willing to expose a Redis style command interface to your clients. Do not adopt it if you need a documented, versioned API with tagged releases, since the repository publishes none, or if your clients need a protocol other than Redis without building a custom service. Before committing, run the ticket example on three nodes with the build and join commands, confirm that the leader elected by the cluster matches the node your client connects to, and read the TLS and auth password sections before binding any address other than 127.0.0.1.

Frequently asked questions

What is tidwall/uhaha?

It is a Go framework for building highly available Raft based data applications, an upgrade to the earlier tidwall/finn project. Under the hood it uses hashicorp/raft, tidwall/redcon and tidwall/wal.

Which protocol does uhaha speak to clients?

The Redis protocol is supported by default, so most Redis client libraries work with it unchanged. You can also build custom services such as HTTP and gRPC through the framework's service APIs.

How do I build and start a uhaha cluster?

Build the example with go build -o ticket examples/ticket/main.go, start the first node with ./ticket -n 1 -a :11001, then start the others with the join flag pointing at an existing member, for example ./ticket -n 2 -a :11002 -j :11001. Three, five or seven nodes are described as ideal.

How do I add and remove servers in a running uhaha cluster?

With the built in RAFT SERVER commands: ADD id address to add a server, REMOVE id to remove one, and PROMOTE or DEMOTE to move a server between non-voter and voter status. RAFT LEADER and RAFT SERVER LIST show the current state.

Is a uhaha cluster secure by default?

A single instance binds to 127.0.0.1 by default, so nothing outside the machine can reach it. To open it up you give it a reachable address, at which point any server on the local network can access the cluster, and you can add an auth password with the --auth flag plus TLS support.

What licence is uhaha released under?

MIT. The repository publishes no GitHub releases, so there is no tagged version to depend on and the main branch is the only reference point.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. tidwall/uhaha on GitHub
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/tidwall-uhaha.svg)](https://hysenlabs.com/projects/tidwall-uhaha)

Was this useful?