Cobra: Making Transactional Key-Value Stores Verifiably Serializable
Reader-compiled version, generated by Claude Opus 5.5 from the authors' source file for an 11th-grade student who knows basic Python, likes pictures and worked examples first, and wants the gist. The authoritative text is the published paper.
1. The problem: can you trust a database you cannot see inside?
(from Abstract, §1, §2.1)
A Python way to picture a "key-value store." Think of it as a giant Python dictionary that lives on someone else's computer:
db = {"apples_in_stock": 5, "alice_balance": 100, "bob_balance": 20}
A transaction is a small group of reads and writes that are supposed to happen together, as one unit. Example: move $10 from Alice to Bob.
# one transaction
a = db["alice_balance"] # read
b = db["bob_balance"] # read
db["alice_balance"] = a - 10 # write
db["bob_balance"] = b + 10 # write
Thousands of programs send transactions like this at the same time. The database runs them in parallel to be fast, but it promises the result will look as if they ran one at a time, in some order. That promise is called serializability.
Many modern cloud databases (the paper names Amazon DynamoDB and Aurora, Azure CosmosDB, CockroachDB, YugaByte DB and others) promise serializability. The paper calls it the "gold-standard" correctness property: lots of application code would be wrong if the database gave a weaker guarantee.
But these databases are black boxes. They are run by someone else, on someone else's machines. Bugs, misconfiguration, or operator mistakes inside them could break the promise, and real production systems have broken it. So the paper asks: can the customers check, from the outside only, that the database really kept its promise?
Cobra is the authors' answer. It records every request sent to the database and every answer that comes back (the history), and a separate verifier checks whether that history could have come from some one-at-a-time order. The paper says Cobra is the first system that combines (a) black-box checking, of (b) serializability, while (c) scaling to real-world online transactional processing workloads.
2. What "serializable" means, with a worked example
(from §2.2–§2.3)
Worked example. Start with db = {"x": 0, "y": 0}. Transaction T1 sets both x and y to 1. Transaction T2 reads both and gets x == 1 but y == 0.
- T2 saw T1's new
x, so T1 must have run first. - T2 saw the old
y, so T2 must have run before T1.
Both cannot be true. It is like seeing money leave Alice's account but never arrive in Bob's: a half-finished transaction leaked out. In the paper's language, the "who must come before whom" arrows form a cycle, and a history is serializable exactly when the arrows can be chosen without any cycle.
Here is the hard part. From outside, you often do not know some of the arrows. The paper's own small example has three transactions on one key x:
T3 read the value 1, so T1 came before T3 (a solid, known arrow). T2 also wrote x. T2 cannot sit between T1 and T3, or T3 would have read 2. So either T2 came after T3, or T2 came before T1. Which one is true is unknown.
The paper calls this structure a polygraph: the arrows you know form the known graph, and each "one of these two" choice is a constraint. The history is serializable if and only if you can pick one option from every constraint so that the whole graph has no cycle. (This "if and only if" is proved in the extended version, Appendix B.)
Why that is hard. Each constraint is a yes/no choice, so n constraints give 2n combinations to consider (the paper writes this as 2|C|). For illustration only: 10 constraints give 210 = 1,024 combinations; 100 give more than 1030. A real history has a huge number of constraints, because every read of a key creates a choice for every other write to that key. Computer scientists call this kind of problem NP-complete: no one knows a way to solve every case quickly.
3. Cobra's core idea: shrink the puzzle, then hand it to a solver
(from §1, §3, §3.1–§3.4, Figure 2)
The authors' starting intuition: although the problem is hard in general, modern solvers (programs that search for an assignment satisfying many logical conditions) often handle real-world cases well. Cobra uses one called MonoSAT, which is good at graph questions such as "is there a cycle?" On its own, though, MonoSAT is still too slow on the brute-force puzzle. So Cobra first shrinks the puzzle with three tricks that use patterns common in real programs:
Trick 1: combining writes (§3.1)
Many transactions read a key and then write the same key (a read-modify-write). The paper's example is a shop:
n = db["apples_in_stock"] # read
db["apples_in_stock"] = n - 1 # write back
If this transaction read the value written by transaction A, then its own write must come right after A's write. That glues the two writes together.
Cobra glues such writes into chains: ordered lists of writes to one key that must be consecutive. Then it only has to decide the order of whole chains, not of every single write. With two chains [W1, W2] and [W3, W4], only two orders remain: [W1, W2] before [W3, W4], or the reverse.
Trick 2: coalescing constraints (§3.2)
In most real workloads there are far more reads than writes. Many constraints end up asking the same underlying question, such as "did write W1 or write W2 happen first?" Cobra merges all those constraints into one bigger either/or choice between two groups of arrows, so the solver makes one decision instead of many.
Trick 3: pruning (§3.3)
If the known arrows already give a path from W2 to R3, then choosing "R3 before W2" would close a loop. That option is impossible, so Cobra settles the constraint without the solver and adds the other option to the known graph. To do this quickly, Cobra needs to know, for every pair of transactions, whether one can reach the other along known arrows. That computation can be done as repeated matrix multiplication, which is exactly what graphics cards (GPUs) are good at, so Cobra runs it on a GPU.
Step 4: solving (§3.4)
Whatever is left goes to MonoSAT, which searches for one choice per constraint that leaves no cycle. If it finds one, the history is accepted. If not (or if Cobra already found a cycle among the known arrows), the history is rejected, and Cobra outputs the problematic transactions as evidence (§5).
The authors prove that this shrunken puzzle is still faithful: an acyclic graph compatible with Cobra's constraints exists if and only if the history is serializable (§3; proof in Appendix B of the extended version).
A question to think about
Why do the tricks work well on "real" workloads but might not on others? (Hint: what happens to trick 1 if almost no transaction reads a key before writing it? Section 6 and the limitations box show what the authors found.)
4. Checking forever: rounds, epochs, and fence transactions
(from §1, §4)
A real database runs all day, so the history never stops growing. Cobra checks it in rounds, each round handling a new piece of history. The catch: in a serializable database, a transaction today could legally read a value written long ago, so the verifier cannot simply forget old transactions; doing so could hide a cycle.
Cobra's fix: every client periodically (for example, every 20 transactions) runs a tiny fence transaction that reads and writes a special key named "EPOCH". Fences divide history into epochs, and they let the verifier prove that sufficiently old transactions are always "before" all future ones. The verifier tracks the frontier (the latest writes that future transactions could still read) and marks older transactions as superseded. Even a superseded transaction is deleted only if it cannot be part of a cycle that future transactions might complete. This garbage collection keeps each round's problem small.
5. What the authors showed
(from §1, §5, §6, Figures 6, 9, 10, 12, 13)
Catching real bugs (Figure 6)
The authors downloaded histories from public bug reports of three real databases, each known to contain a serializability violation, and fed them to Cobra. It found all five.
| Violation | Database | #Txns | Time |
|---|---|---|---|
| G2-anomaly | YugaByteDB 1.3.1.0 | 37.2k | 66.3s |
| Disappearing writes | YugaByteDB 1.1.10.0 | 2.8k | 5.0s |
| G2-anomaly | CockroachDB-beta 20160829 | 446 | 1.0s |
| Read uncommitted | CockroachDB 2.1 | 20* | 1.0s |
| Read skew | FaunaDB 2.5.4 | 8.2k | 11.4s |
Speed compared with other checkers (Figure 5, Figure 9)
The paper's Figure 5 plots verification time (seconds, 0–14, lower is better) against number of transactions (0–10k) for Cobra and four baseline checkers on one benchmark. Cobra's line is lowest, and time grows faster than linearly as the history grows. The exact plotted values are not reproduced here; see the published PDF.
For strict serializability (a stricter version where the order must also respect real clock time), on 2,000 transactions with a clock drift threshold of 100 ms, Cobra beat the baselines by 45× and 107× in verification time.
Keeping up over time (Figure 10)
Running round after round, the best verification capacity was 2.3k txn/sec for one test workload (BlindW-RM, at 5k transactions per round) and 1.2k txn/sec for an eBay-like auction workload (C-RUBiS, at 2.5k per round). The paper's headline 2k txn/sec corresponds to 170M per day. For comparison the paper cites Apple Pay at 33M txn/day and Visa at 150M txn/day, while noting that a "transaction" there means something slightly different, so the comparison is inexact. The verifier only needs to match the database's average load, not its peak, because it works in the background and can catch up.
Cost to the people using the database (Figures 12, 13)
- RocksDB: 90th-percentile latency (waiting time) rose 2×, with a 50% throughput penalty, which the authors attribute to recording history on the same disk the database uses.
- PostgreSQL: minor overhead.
- Google Datastore: a throughput penalty, reflecting the service's limit on operations per second plus the extra operations from fence transactions.
- Network: the extra traffic was 2.17%–7.28% across the four workloads measured.
Optional: the algorithm in Python (Figure 3), and how big Cobra's code is
Translation of the paper's pseudocode. Cobra's actual implementation is in Java and CUDA/C++ (Figure 4). [added for you: translated code]
Each line ends with the original line number. Helpers such as Graph, transitive_closure, all_pairs, reject, and add_session_order_edges are left undefined, just as in the paper. Two Python-only details: the paper's "Set" of constraints is a Python list here (a pair of sets cannot go inside a Python set), and line 78 loops over a copy, list(con), because Python does not allow removing items from a list while looping over that same list.
def construct_encoding(history): # 1
g, readfrom, wwpairs = create_known_graph(history) # 2
con = gen_constraints(g, readfrom, wwpairs) # 3
con, g = prune(con, g) # §3.3, executed one or more times # 4
return con, g # 5
# 6
def create_known_graph(history): # 7
g = Graph() # the known graph # 8
wwpairs = {} # {(key, tx): tx} consecutive writes # 9
readfrom = {} # {(key, tx): set of tx} maps a write to its readers # 10
for tx in history: # 11
g.nodes.add(tx) # 12
for rop in tx.reads: # 13
g.edges.add((rop.read_from_tx, tx)) # read-dependencies # 14
readfrom.setdefault((rop.key, rop.read_from_tx), set()).add(tx) # 15
# 16
# detect RMW (read-modify-write) transactions # 17
for key in tx.keys_both_read_and_written(): # 18
rop = tx.read_of(key) # 19
if wwpairs.get((key, rop.read_from_tx)) is not None: # 20
reject() # multiple consecutive writes, not serializable # 21
wwpairs[(key, rop.read_from_tx)] = tx # 22
# 23
add_session_order_edges(g) # §4.2 # 24
return g, readfrom, wwpairs # 25
# 26
def gen_constraints(g, readfrom, wwpairs): # 27
# each key maps to set of chains; each chain is an ordered list # 28
chains = {} # {key: list of chains} # 29
for tx in g.nodes: # 30
for wrop in tx.writes: # 31
chains.setdefault(wrop.key, []).append([tx]) # one-element list # 32
# 33
combine_writes(chains, wwpairs) # §3.1 # 34
infer_rw_edges(chains, readfrom, g) # infer anti-dependency # 35
# 36
con = [] # 37
for key, chainset in chains.items(): # 38
for chain_i, chain_j in all_pairs(chainset): # 39
con.append(coalesce(chain_i, chain_j, key, readfrom)) # §3.2 # 40
# 41
return con # 42
def combine_writes(chains, wwpairs): # 43
for (key, tx1), tx2 in wwpairs.items(): # 44
# By construction of wwpairs, tx1 is the write immediately # 45
# preceding tx2 on key. Thus, we can sequence all writes # 46
# prior to tx1 before all writes after tx2, as follows: # 47
chain1 = next(c for c in chains[key] if c[-1] == tx1) # 48
chain2 = next(c for c in chains[key] if c[0] == tx2) # 49
chains[key].remove(chain1); chains[key].remove(chain2) # 50
chains[key].append(chain1 + chain2) # 51
# 52
def infer_rw_edges(chains, readfrom, g): # 53
for key, chainset in chains.items(): # 54
for chain in chainset: # 55
for i in range(len(chain) - 1): # i in [0, len(chain)-2] # 56
for rtx in readfrom.get((key, chain[i]), set()): # 57
if rtx != chain[i + 1]: g.edges.add((rtx, chain[i + 1])) # 58
# 59
def coalesce(chain1, chain2, key, readfrom): # 60
edge_set1 = gen_chain_to_chain_edges(chain1, chain2, key, readfrom) # 61
edge_set2 = gen_chain_to_chain_edges(chain2, chain1, key, readfrom) # 62
return (edge_set1, edge_set2) # 63
# 64
def gen_chain_to_chain_edges(chain_i, chain_j, key, readfrom): # 65
if not readfrom.get((key, chain_i[-1])): # tail has no readers # 66
edge_set = {(chain_i[-1], chain_j[0])} # tail -> head # 67
return edge_set # 68
# 69
edge_set = set() # 70
for rtx in readfrom[(key, chain_i[-1])]: # 71
edge_set.add((rtx, chain_j[0])) # 72
return edge_set # 73
# 74
def prune(con, g): # 75
# tr is the transitive closure (reachability of every two nodes) of g # 76
tr = transitive_closure(g) # standard algorithm; see [70, Ch.25] # 77
for c in list(con): # c = (edge_set1, edge_set2) # 78
if any(tr.reaches(tx_j, tx_i) for (tx_i, tx_j) in c[0]): # 79
g.edges |= c[1] # 80
con.remove(c) # 81
elif any(tr.reaches(tx_j, tx_i) for (tx_i, tx_j) in c[1]): # 82
g.edges |= c[0] # 83
con.remove(c) # 84
return con, g # 85
Implementation size (from §5, Figure 4):
| Component | Part | Size |
|---|---|---|
| Client library | history recording | 620 lines of Java |
| Client library | database adapters | 900 lines of Java |
| Verifier | data structures and algorithms | 2k lines of Java |
| Verifier | GPU optimizations | 550 lines of CUDA/C++ |
| Verifier | history parser and others | 1.2k lines of Java |
Limitations (what Cobra cannot do)
(from §1, §2.1, §3.5, §6.1, §6.2, §8)
- No time guarantee. There is no guarantee Cobra finishes in reasonable time; in the worst case the running time is in principle exponential, because the problem is NP-complete. In the authors' experiments on real workloads, it did finish. (L1)
- Key-value operations only. Cobra does not handle range queries or SQL operations such as join and sum unless they are first translated into key-value reads and writes. (L2)
- Client style. Client programs may use multiple threads but not the async / event-driven style. (L3)
- The checker itself must not crash. The verifier and collectors are assumed never to crash; making them fault-tolerant is left to future work. (L4)
- No new bugs found. The authors did not find serializability violations "in the wild." In their words: "we lack a sensational headline." (L5)
- Slow on write-heavy workloads. Cobra can be slow when there are many blind writes (writes with no read of the same key first), as in the BlindW-WM test. On BlindW-RW and BlindW-WM, the history eventually ran out of GPU memory in the scaling experiment. (L6)
- Detects, does not prevent. Cobra can only tell you a violation happened; it cannot stop it. For strict serializability it assumes the collectors' clocks differ by less than a threshold (100 ms by default); if they differ by more, it may wrongly reject a history that was actually fine. (L7)
- One trust domain, full history. Clients, collectors, and verifier must all be on the same trusted side, and the verifier needs the complete history of requests and responses. (L8)
After publication: not part of the paper
Who cites this paper
- 72 citations according to Semantic Scholar, checked 2026-09-30. Other indexes (e.g. Google Scholar) usually report more. A second source could not be checked on that date.
- No primary source documents industrial adoption of Cobra itself. Jepsen, the most widely used database-testing framework, uses its own checker, Elle.
Work that builds on it
Black-box checking of transactional isolation became an active line of work after 2020. Each relation below is stated as that paper reports it.
| Work | Venue | Relation to Cobra (as reported by that work) |
|---|---|---|
| PolySI (Huang, Liu, Chen, Wei, Basin, Li, Pan) | PVLDB 2023 | Carries the polygraph-plus-MonoSAT approach from serializability to snapshot isolation; compares against a Cobra-based SI checker. |
| Viper (Zhang, Ji, Mu, Tan) | EuroSys 2023 | Snapshot-isolation checker that integrates Cobra's combining-writes and coalescing-constraints optimizations. |
| Plume (Liu, Gu, Wei, Basin) | OOPSLA 2024 | Sound and complete checking of weak isolation levels; compares against Cobra. |
| Emme / King Cobra (Clark, Rigger, Wickerson, Donaldson) | EuroSys 2024; TOCS 2026 | Recovers version certificates from the database; the TOCS extension builds "King Cobra," a modified Cobra that can use version order. |
| AWDIT (Møldrup, Pavlogiannis) | PLDI 2025 | Optimal-complexity weak-isolation tester; generates evaluation histories with Cobra's benchmarks. |
| Chronos / Aion (Li, Wei, Ouyang, Chen, Yang, Zhang, Pan) | ICDE 2025 | Online checking from database timestamps; does not need Cobra's fence transactions. |
| MTC (Wei, Xiao, Yang, Liu, Yin, Chen, Pan) | ICDE 2025 | Restricts workloads to mini-transactions; reports large end-to-end gains over Cobra. |
| Vbox (Sun, Zou) | arXiv 2025 | "Developed from Cobra"; adds predicate reads and writes; reports 60–100× over Cobra on 10K-transaction histories. |
| VeriStrong (Cai, Liu, Wei, Chen, Pan) | PVLDB 2026 | Drops the unique-value assumption; reports up to 4.4× over GPU-accelerated Cobra. |
| Boomslang (Tan, Zhang, Mu) | arXiv 2026 | A framework that generalizes Cobra; reimplements Cobra as one module. |
State of the art, in one paragraph
Defining isolation levels is largely settled; checking them efficiently is the active problem. Checking serializability and snapshot isolation on a black box is NP-complete in general. Since Cobra, the field has escaped that hardness by three routes: assuming more about the system (timestamps or version certificates from the database); restricting the workload (e.g. mini-transactions); and improving the solver and the encoding. Reported speedups over the previous generation are now one to three orders of magnitude. The weak isolation levels have sound and complete checkers. The open edge is real SQL: with predicates, even cheap levels become hard, and a black-box checker for a genuine SQL workload does not quite exist yet. A maintained comparison of checkers is at naizhengtan.github.io/pages/isolation-checker-matrix.html.
Faithfulness note
Reader profile used: 11th-grade student, one intro Python class, visual learner, wants the gist; no background in databases, graphs, solvers, or GPUs.
Ledger items included
- Claims: C1 (first to combine black-box, serializability, scale) in §1 above; C2 (10× verification cost, not throughput), C3 (10k in 14 s vs 1k or less), C4 (2k txn/sec = 170M/day, with Apple Pay / Visa comparison and its caveat), C5 (all five real bugs, full Figure 6 table), C6 (45× and 107×), C7 (2.3k and 1.2k txn/sec), C8 (RocksDB / PostgreSQL / Datastore overhead), C9 (2.17%–7.28% network) in section 5; C10 (if-and-only-if validity) in sections 2 and 3; C11 (combining writes, coalescing, GPU pruning, MonoSAT, fences and epochs) in sections 3 and 4; C12 (implementation size table) in the optional code panel.
- Limitations: L1–L8, all in the Limitations box, shortened and reworded in plain language.
Ledger items omitted
- None. (Paper content outside the ledger was left out as beyond a gist-level read for this reader: the exact formal definitions and the size formula for |C|, the details of the strict-serializability algorithm, the full garbage-collection conditions and the epoch Guarantee proof, benchmark and hardware setup, Figures 7, 8, 11 and the per-workload rows of Figure 13, related work, and the "Who would use Cobra" discussion.)
Additions (not the authors' words)
- Bridges and analogies: the key-value store as a Python dictionary; the Alice-to-Bob money transfer; the "half-finished transfer" picture of a cycle; the 2n explanation with the illustrative numbers 210 = 1,024 and "more than 1030" for 100 constraints (simple arithmetic, not results about Cobra); the plain definition of "solver" and "NP-complete."
- Worked examples: the T1/T2 x-and-y cycle example and its diagram (invented for illustration); the apples read-modify-write code (based on the paper's shopping example).
- Redrawn diagrams: Figure 1 (architecture), the §2.3 polygraph example, Figure 2 (pipeline, re-laid out), the §3.3 pruning example; a bar chart drawn from the Figure 6 table values; a simplified, invented epoch/fence timeline.
- Translated code: Figure 3 in Python, line-for-line with original line numbers; two Python-only adaptations noted in the panel (list instead of set for constraints; looping over a copy in line 78).
- A question for the reader at the end of section 3.
- No numbers were derived or computed about Cobra; Figure 5, 7, 8, 9, 10, 11, and 12 plot values were not drawn.