PySyncObj: replicating a Python class across servers with Raft
A library for replicating your python class between multiple servers, based on raft protocol
At a glance
- What is it?
- PySyncObj turns an ordinary Python class into a replicated one by inheriting from SyncObj and decorating the methods that mutate state, with a Raft implementation for leader election and log replication. It ships batteries for counters, dicts and locks, and the interface is deliberately small.
- Who is it for?
- Adopt PySyncObj when you want fault tolerance in Python without adopting a consensus implementation or a service mesh, since the whole integration is one base class, one decorator and a list of partner addresses, and the shipped counter, dict and lock cover common cases without you writing them.
- 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 56 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One base class and one decorator turn a class into a replicated one
PySyncObj is a Python library for building fault-tolerant distributed systems, and the ability it provides is replicating your application data between multiple servers. The integration surface is what makes it interesting, and it is very small.
You take an ordinary class, inherit from SyncObj, pass your own address and the addresses of your partners to the constructor, and mark every method that modifies your fields with the @replicated decorator. That is the whole contract.
The before and after of a counter makes the shape clear. The plain class is just a class:
class MyCounter(object):
def __init__(self):
self.__counter = 0
def incCounter(self):
self.__counter += 1
def getCounter(self):
return self.__counterAnd the replicated version inherits from SyncObj, passes the addresses, and decorates the mutating method:
class MyCounter(SyncObj):
def __init__(self):
super(MyCounter, self).__init__('serverA:4321', ['serverB:4321', 'serverC:4321'])
self.__counter = 0
@replicated
def incCounter(self):
self.__counter += 1
def getCounter(self):
return self.__counterThe read method is not decorated, and that is the point. The write path goes through the consensus layer and is replicated, while the read path is local and fast, which is the standard tradeoff in any replicated log system.
The address convention is spelled out in the README. If you have serverA, serverB and serverC on port 4321, the application on serverA uses the self address serverA:4321 with partners serverB:4321 and serverC:4321, and each node does the same with the others in the partner list. The ordering of partners does not matter, and every node is configured with the same set.
The library is MIT licensed, authored by Filipp Ozinov, the default branch is master, and the last push was on 2026-08-30.
The feature list, and what each entry costs you
The README lists the features, and reading them in order tells you what the design is built on.
The foundation is the raft protocol, used for leader election and log replication. On top of that sits log compaction, which uses fork for copy-on-write while serializing data on disk. Dynamic membership changes are supported, either through the syncobj_admin utility or directly from your code, and the README links both to wiki pages.
Zero downtime deploy is the feature that changes how you would run this in production. The point is that there is no need to stop the cluster to update nodes, and there is a whole wiki page for it, plus a test_zerodowntime/ directory in the repository, which is the kind of thing you only see when the feature is tested rather than asserted.
Storage is a choice. In-memory and on-disk serialization are both available, and the guidance is to use in-memory mode for small data and on-disk for big one. Encryption is a password you can set for use in an external network.
The portability claim is that Python 2 and Python 3 work on Linux, macOS and Windows with no dependencies required, only optional ones such as cryptography. And the event loop is configurable: it can run in a separate thread with its own event loop, or you can call the onTick function inside your own one, which matters if your application already has a loop and does not want a second one.
The last item on the list is the one the whole design serves. The interface is convenient, and you can easily transform an arbitrary class into a replicated one.
Batteries for the data structures you would otherwise write twice
If you only need some distributed data structures, the library ships them rather than making you implement replicated counters yourself. The batteries live in pysyncobj.batteries and are passed to SyncObj as consumers.
from pysyncobj import SyncObj
from pysyncobj.batteries import ReplCounter, ReplDict
counter1 = ReplCounter()
counter2 = ReplCounter()
dict1 = ReplDict()
syncObj = SyncObj('serverA:4321', ['serverB:4321', 'serverC:4321'], consumers=[counter1, counter2, dict1])
counter1.set(42, sync=True) # set initial value to 42, 'sync' means that operation is blocking
counter1.add(10, sync=True) # add 10 to counter value
counter2.inc(sync=True) # increment counter value by one
dict1.set('testKey1', 'testValue1', sync=True)
dict1['testKey2'] = 'testValue2' # this is basically the same as previous, but asynchronous (non-blocking)
print(counter1, counter2, dict1['testKey1'], dict1.get('testKey2'))The sync flag is the API decision worth understanding. With sync=True the operation is blocking, so the call returns when the write has been committed through the replication path. Without it, the operation is asynchronous, and the item assignment in the example is the non-blocking form of the same set call.
That distinction matters for correctness. An asynchronous write can be lost if the node fails before replication completes, and if your application logic assumes the write landed, the blocking form is what you want. A hybrid, where you take the blocking hit on critical writes and stay asynchronous on high volume ones, is a reasonable pattern and the API supports it directly.
There is also a replicated lock, built on a ReplLockManager with an the release timeout:
from pysyncobj import SyncObj
from pysyncobj.batteries import ReplLockManager
lockManager = ReplLockManager(the release timeout=75) # Lock will be released if connection dropped for more than 75 seconds
syncObj = SyncObj('serverA:4321', ['serverB:4321', 'serverC:4321'], consumers=[lockManager])
if lockManager.tryAcquire('testLockName', sync=True):
# do some actions
lockManager.release('testLockName')The the release timeout setting is the interesting part, because a lock that survives a crashed holder is a deadlock. Releasing after 75 seconds of disconnection turns that into a recoverable state, at the cost of a lock that can expire while the holder is still working.
Performance figures, and the packaging behind them
The performance section is two images with captions, and the captions carry the numbers. The first is RPS against cluster size, with 15K rps on 3 nodes and 14K rps on 7 nodes. The second is RPS against request size, with 22K rps on 10 byte requests and 5K rps on 20Kb requests.
Those are the only performance numbers the project publishes, so read them for what they are. Throughput holds roughly flat as the cluster grows, which is what you would hope to see from a replicated write path, and the request size curve is dominated by the size of the payload being replicated. No latency percentiles are given, and no hardware is described, so these numbers cannot be transferred to another environment without measuring.
The packaging is old-fashioned in a way that matters more than it looks. The setup.py reads the version from pysyncobj.version rather than declaring it, and it builds the long description by converting README.md with pypandoc, falling back to the short description if pypandoc is not installed or the conversion fails. The dependency list is empty, which is the origin of the no dependencies claim, and the optional cryptography is not listed here.
The classifiers list Python 2.7, 3.4 and 3.5, and the operating systems as Linux and macOS X, which is stale relative to the README's claim about Windows. The console script entry point registers syncobj_admin from pysyncobj.syncobj_admin:main, and the repository also carries a syncobj_admin.py at the top level, which is the utility the README points at for dynamic membership changes.
The repository layout is small and understandable: the pysyncobj/ package, examples/, benchmarks/, docs/, test_syncobj.py, the test_zerodowntime/ directory, appveyor.yml for Windows builds, a .coveragerc and a Coveralls badge. The examples are counter.py, kvstorage.py, kvstorage_http.py and lock.py, and the unit tests live in a single test_syncobj.py rather than a directory of them.
Where PySyncObj is the wrong tool
The first wrong case is a workload that needs a real database. PySyncObj replicates a class, which means it replicates whatever state you put in it, and it gives you no query language, no secondary indexes, no transactions across objects, and no schema evolution story. If your problem is a key value store, the kvstorage.py example shows the shape of what you would write, and you would be writing the storage layer too.
The second is a latency-sensitive or geographically spread cluster. The addresses are host:port pairs on a LAN, and the replication path is a Raft log over TCP. Spanning continents with that design gives you round trip times in the hundreds of milliseconds on every write, and Raft's leader election does not get better with distance. Nothing in the documentation claims otherwise, but it is worth naming.
The third is a team that wants a maintained consensus implementation with a large community around it. This is a single maintainer project, and the release picture shows the drift. The release badge in the README still reads 0.3.15 while the release list shows 0.3.15 on 2026-03-17, 0.3.16 on 2026-08-14 and 0.3.17 on 2026-08-30, so the badge lags and the classifiers still advertise Python 2.7 through 3.5.
The fourth is a specific operational risk of the Raft approach itself. Log compaction here uses fork for copy-on-write while serializing data on disk, which is a Unix-specific mechanism, and the on-disk mode is what you need for large data. A workload with a large state and a high write rate is where that combination gets expensive.
There is also a security point. The encryption feature is described as setting a password for use in an external network, and it depends on the optional cryptography package rather than being built in. If your cluster spans a network you do not control, that arrangement deserves a look before you trust it.
Editorial conclusion
Adopt PySyncObj when you want fault tolerance in Python without adopting a consensus implementation or a service mesh, since the whole integration is one base class, one decorator and a list of partner addresses, and the shipped counter, dict and lock cover common cases without you writing them. Step back if you need a database with a query language, a cross-region latency story, or a maintained raft implementation with a large community, because this is a single maintainer library whose release badge in the README still shows 0.3.15 while the release list has reached 0.3.17. Before you deploy it, read the zero downtime deploy and dynamic membership pages in the wiki, and decide whether the in-memory or on-disk serialization mode fits the size of the data you intend to replicate.
Frequently asked questions
How do I make a Python class replicated with PySyncObj?
Inherit from SyncObj, call its constructor with your own address and the list of partner addresses, and decorate every method that modifies your fields with the @replicated decorator. Read methods stay local and undecorated, so you can call the mutating method on one node and read the value on another.
What is the difference between sync=True and an asynchronous write in PySyncObj?
With sync=True the operation is blocking and returns once the write has been replicated. Without it the operation returns immediately, so the write can be lost if the node fails before replication completes. The batteries such as ReplCounter and ReplDict support both forms.
What data structures does PySyncObj ship with?
The batteries include ReplCounter, ReplDict and ReplLockManager, and you pass them to SyncObj as consumers. The lock manager takes an the release timeout, so a lock is released if the connection drops for longer than that many seconds rather than deadlocking.
Can PySyncObj run on Windows and does it need any dependencies?
The README states that Python 2 and Python 3 are supported on Linux, macOS and Windows with no dependencies required, and that only optional ones such as cryptography are needed. The setup.py classifiers, however, still list only Python 2.7 to 3.5 and Linux with macOS X.
Official sources
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.
[](https://hysenlabs.com/projects/bakwc-pysyncobj)Was this useful?