Cobra: Making Transactional Key-Value Stores Verifiably Serializable

Cheng Tan, Changgeng Zhao, Shuai Mu, and Michael Walfish

NYU Department of Computer Science, Courant Institute · Stony Brook University (Shuai Mu)

OSDI 2020 · published paper · extended version, arXiv:1912.09018

Reader-compiled version, generated by Claude Opus 5.5 from the authors' source file for a visual-first backend engineer who runs a service on a cloud database, reads Java, knows transactions and SQL in practice (no theory), and wants the ten-minute gist: what is this, and should my team care. The authoritative text is the published paper.

1. The one-picture version

(from Abstract, §1, §2.1, Figure 1)
Your trust domain (you deploy all of this) Client Client Client History collectors record every request and response Cloud database untrusted black box Verifier accept / reject
Redrawn from Figure 1. The dashed box is a trust domain. The verifier is off the critical path but must keep up on average.

Cloud databases (the paper names Amazon DynamoDB and Aurora, Azure CosmosDB, CockroachDB, YugaByte DB, and others) offer serializable transactions: all transactions appear to execute in a single, sequential order. Your application code probably relies on that. But you cannot see inside a hosted database, and production systems have exhibited serializability violations.

Cobra lets the client side check. History collectors record every request your clients send and every response the database returns. A verifier, running on your own hardware in the background, decides whether that history could have come from some serial order. The database is unmodified and does not know it is being checked.

The paper's headline claim (C1): Cobra is the first system that combines (a) black-box checking, of (b) serializability, while (c) scaling to real-world online transactional processing workloads.

Each client request is one of five operations: start, commit, abort, read(key), write(key). The paper's word for "database" always means a transactional key-value store.

2. How it checks: one worked example

(from §2.2–§2.3, §3, the paper's inline polygraph figure, Figure 2)
T1 W1(x=1) T3 R3(x) → 1 T2 W2(x=2) wr(x) known edge option A: T3 → T2 option B: T2 → T1 dashed pair joined by an arc = one constraint (pick exactly one)
Redrawn from the paper's example polygraph (§2.3). Solid arrow: known read-dependency. Dashed arrows joined by an arc: one constraint.

What the example says. T3 read x from T1, so T1 comes before T3: that edge is known. T2 also wrote x, so T2 cannot fall between T1 and T3 (otherwise T3 would have read 2). So either T2 comes after T3, or T2 comes before T1. The history does not say which. The paper calls this pair a constraint; the graph of certain edges is the known graph; together they form a polygraph.

The check. The history is serializable if you can pick one option from every constraint and end up with a graph that has no cycle (a cycle would mean "A before B before A"). If every combination of choices creates a cycle, there is no valid serial order and the verifier rejects. The paper proves this encoding is exact (C10): an acyclic graph compatible with Cobra's constraints exists if and only if the history is serializable. On a reject, Cobra produces a certificate: a cycle in the known graph, or a set of unsatisfiable clauses from the solver (§5).

Why this is hard. Each constraint is a binary choice, so brute force faces 2|C| possibilities, and real histories have many constraints. Checking black-box serializability is NP-complete.

verifier round i history collectors create known graph combining writes coalescing constraints pruning (GPU) MonoSAT solver accept or reject garbage collection carries graph g from round i−1 into round i
Redrawn from Figure 2: the verifier's process, within a round and across rounds. Shaded boxes are Cobra's own techniques.

How Cobra makes it tractable (C11). It shrinks the search before handing it to MonoSAT, an SMT solver suited to graph properties such as acyclicity:

W1 R2 W2 W3 R4 W4 ? chain [W1, W2] vs chain [W3, W4]: only two orders remain
Redrawn from the paper's combining-writes figure (§3.1, right side). R2 reads from W1 and R4 reads from W3, so each pair becomes a chain.
Optional: the encoding algorithm (Figure 3) in Java

[added for you] Translation of the paper's pseudocode. Cobra's actual implementation is in Java and CUDA/C++ (Figure 4). Line-for-line with the paper; original line numbers are in the left comments. Types such as Graph, Tx, Key, Pair, Edge, Constraint stand in for the pseudocode's data structures and are not defined here.

/* 1*/ Encoding constructEncoding(History history) {
/* 2*/   Known k = createKnownGraph(history); Graph g = k.g;
/* 3*/   Set<Constraint> con = genConstraints(g, k.readfrom, k.wwpairs);
/* 4*/   Encoding e = prune(con, g); con = e.con; g = e.g;      // §3.3, executed one or more times
/* 5*/   return new Encoding(con, g);
/* 6*/ }
/* 7*/ Known createKnownGraph(History history) {
/* 8*/   Graph g = new Graph();                                    // the known graph
/* 9*/   Map<Pair<Key, Tx>, Tx> wwpairs = new HashMap<>();         // consecutive writes
/*10*/   Map<Pair<Key, Tx>, Set<Tx>> readfrom = new HashMap<>();   // maps a write to its readers
/*11*/   for (Tx tx : history) {
/*12*/     g.nodes.add(tx);
/*13*/     for (ReadOp rop : tx.reads()) {
/*14*/       g.edges.add(new Edge(rop.readFromTx, tx));            // read-dependencies
/*15*/       readfrom.computeIfAbsent(Pair.of(rop.key, rop.readFromTx), x -> new HashSet<>()).add(tx); }
/*16*/
/*17*/     // detect RMW (read-modify-write) transactions
/*18*/     for (Key key : tx.keysBothReadAndWritten()) {
/*19*/       ReadOp rop = tx.readOf(key);
/*20*/       if (wwpairs.get(Pair.of(key, rop.readFromTx)) != null)
/*21*/         reject();   // multiple consecutive writes, not serializable
/*22*/       wwpairs.put(Pair.of(key, rop.readFromTx), tx); }
/*23*/   }
/*24*/   g.addSessionOrderEdges();                                 // §4.2
/*25*/   return new Known(g, readfrom, wwpairs);
/*26*/ }
/*27*/ Set<Constraint> genConstraints(Graph g, ReadFromMap readfrom, WWPairs wwpairs) {
/*28*/   // each key maps to set of chains; each chain is an ordered list
/*29*/   Map<Key, Set<List<Tx>>> chains = new HashMap<>();
/*30*/   for (Tx tx : g.nodes)
/*31*/     for (WriteOp wrop : tx.writes())
/*32*/       chains.computeIfAbsent(wrop.key, x -> new HashSet<>()).add(List.of(tx)); // one-element list
/*33*/
/*34*/   combineWrites(chains, wwpairs);       // §3.1
/*35*/   inferRWEdges(chains, readfrom, g);    // infer anti-dependency
/*36*/
/*37*/   Set<Constraint> con = new HashSet<>();
/*38*/   for (var ent : chains.entrySet()) { Key key = ent.getKey(); Set<List<Tx>> chainset = ent.getValue();
/*39*/     for (Pair<List<Tx>, List<Tx>> p : everyPair(chainset))
/*40*/       con.add(coalesce(p.first, p.second, key, readfrom)); }   // §3.2
/*41*/
/*42*/   return con; }
/*43*/ void combineWrites(Map<Key, Set<List<Tx>>> chains, WWPairs wwpairs) {
/*44*/   for (var ent : wwpairs.entrySet()) { Key key = ent.getKey().first; Tx tx1 = ent.getKey().second; Tx tx2 = ent.getValue();
/*45*/     // By construction of wwpairs, tx1 is the write immediately
/*46*/     // preceding tx2 on key. Thus, we can sequence all writes
/*47*/     // prior to tx1 before all writes after tx2, as follows:
/*48*/     List<Tx> chain1 = listWhoseLastElemIs(chains.get(key), tx1);
/*49*/     List<Tx> chain2 = listWhoseFirstElemIs(chains.get(key), tx2);
/*50*/     chains.get(key).removeAll(Set.of(chain1, chain2));
/*51*/     chains.get(key).add(concat(chain1, chain2)); }
/*52*/ }
/*53*/ void inferRWEdges(Map<Key, Set<List<Tx>>> chains, ReadFromMap readfrom, Graph g) {
/*54*/   for (var ent : chains.entrySet()) { Key key = ent.getKey(); Set<List<Tx>> chainset = ent.getValue();
/*55*/     for (List<Tx> chain : chainset)
/*56*/       for (int i = 0; i <= chain.size() - 2; i++)
/*57*/         for (Tx rtx : readfrom.getOrDefault(Pair.of(key, chain.get(i)), Set.of()))
/*58*/           if (rtx != chain.get(i + 1)) g.edges.add(new Edge(rtx, chain.get(i + 1))); }
/*59*/ }
/*60*/ Constraint coalesce(List<Tx> chain1, List<Tx> chain2, Key key, ReadFromMap readfrom) {
/*61*/   Set<Edge> edgeSet1 = genChainToChainEdges(chain1, chain2, key, readfrom);
/*62*/   Set<Edge> edgeSet2 = genChainToChainEdges(chain2, chain1, key, readfrom);
/*63*/   return new Constraint(edgeSet1, edgeSet2);
/*64*/ }
/*65*/ Set<Edge> genChainToChainEdges(List<Tx> chainI, List<Tx> chainJ, Key key, ReadFromMap readfrom) {
/*66*/   if (readfrom.getOrDefault(Pair.of(key, tail(chainI)), Set.of()).isEmpty()) {
/*67*/     Set<Edge> edgeSet = Set.of(new Edge(tail(chainI), head(chainJ)));
/*68*/     return edgeSet; }
/*69*/
/*70*/   Set<Edge> edgeSet = new HashSet<>();
/*71*/   for (Tx rtx : readfrom.get(Pair.of(key, tail(chainI))))
/*72*/     edgeSet.add(new Edge(rtx, head(chainJ)));
/*73*/   return edgeSet;
/*74*/ }
/*75*/ Encoding prune(Set<Constraint> con, Graph g) {
/*76*/   // tr is the transitive closure (reachability of every two nodes) of g
/*77*/   Reachability tr = transitiveClosure(g);   // standard algorithm; see [70, Ch.25]
/*78*/   for (Iterator<Constraint> it = con.iterator(); it.hasNext(); ) { Constraint c = it.next();
/*79*/     if (c.edgeSet1.stream().anyMatch(ed -> tr.reaches(ed.to, ed.from))) {
/*80*/       g.edges.addAll(c.edgeSet2);
/*81*/       it.remove(); }                                          // con −= c
/*82*/     else if (c.edgeSet2.stream().anyMatch(ed -> tr.reaches(ed.to, ed.from))) {
/*83*/       g.edges.addAll(c.edgeSet1);
/*84*/       it.remove(); } }                                        // con −= c
/*85*/   return new Encoding(con, g); }

3. Running it continuously next to production

(from §2.1, §4, §6.2)
older epochs: can be garbage-collected (when safe) session 1 session 2 session 3 fence transaction (read-modify-write of key "EPOCH") time →
Schematic, [added for you]: drawn from the description in §4.2–§4.3, not a figure in the paper. Positions are illustrative, not data.

A real service never stops, so the history grows forever. Cobra verifies in rounds, each covering a portion of the history. The catch: under serializability a future transaction may legally read an old value, so the verifier cannot simply forget old transactions.

The fix: each client periodically issues a fence transaction (for example, every 20 transactions), a read-modify-write of a dedicated key named "EPOCH". Fences divide history into epochs. Because practical serializable databases preserve session order (the paper names PostgreSQL, Azure Cosmos DB, and Google Cloud Datastore), fences cannot all be pushed to the start of the serial order. That lets the verifier prove that sufficiently old transactions are superseded (no future transaction can read them; the boundary is the frontier) and, when a further safety check passes, delete them.

Sizing. The verifier's capacity must be at least the database's average load over a long interval, such as a day, not its peak: it is off the critical path and can catch up. More frequent fences make the verifier faster but cost peak client throughput.

4. The numbers

(from Abstract, §1, §5, §6.1–§6.4, Figures 4, 6, 9, 10, 12, 13)
5 of 5
real serializability bugs from bug reports detected (C5)
Figure 6
10×
improvement over baselines in verification cost: the problem size handled for a given time budget. Not throughput. (C2)
§1, §6.4
10k txns in 14 s
checked by Cobra; baselines handle only 1k or less in the same time budget (C3)
§1
2k txn/sec
sustainable verification throughput on the tested workloads, "equivalent to 170M/day" (C4)
Abstract, §6.4
2.3k / 1.2k txn/sec
best verification capacity: BlindW-RM at 5k txns per round; C-RUBiS at 2.5k per round (C7)
Figure 10
45× and 107×
faster verification than baselines when checking strict serializability under clock drift (2,000 txns of BlindW-RW, 100 ms threshold) (C6)
Figure 9
2.17%–7.28%
network overhead, per 1k transactions, across four workloads (C9)
Figure 13

Scale comparison, with the paper's own caveat. The paper compares 170M/day with Apple Pay at 33M txn/day and Visa at 150M txn/day, and notes that a payment "transaction" might translate to several database transactions, so the comparison is inexact.

Detecting real bugs

G2-anomaly · YugaByteDB 1.3.1.0 · 37.2k txns 66.3 s Disappearing writes · YugaByteDB 1.1.10.0 · 2.8k txns 5.0 s G2-anomaly · CockroachDB-beta 20160829 · 446 txns 1.0 s Read uncommitted · CockroachDB 2.1 · 20* txns 1.0 s Read skew · FaunaDB 2.5.4 · 8.2k txns 11.4 s * The bug report only contains a small fragment of the history.
Redrawn from the table in Figure 6 (bar lengths proportional to the table's "Time" column). Histories downloaded from the databases' bug repositories and fed to Cobra's verifier.

What it costs your clients

Measured on the C-Twitter benchmark against the unmodified client library, e.g. plain JDBC (C8, Figure 12):

The latency-vs-throughput curves themselves (Figure 12) and the runtime plots (Figures 5, 7–11) are not reproduced in the source; see the published PDF.

Figure 13: network and storage overheads per 1k transactions
workloadnetwork trafficnetwork %history size
BWrite-RW227.4 KB7.28%245.5 KB
C-Twitter292.9 KB4.46%200.7 KB
C-RUBiS107.5 KB4.53%148.9 KB
TPC-C78.2 KB2.17%1380.8 KB

What it is made of (C12, Figure 4)

The client library is Java and wraps JDBC, the Google Datastore library, and RocksJava. It adds a unique id to each write (and strips it on reads), issues fence transactions, and records history to disk. The verifier ran on a p3.2xlarge Amazon EC2 instance with an NVIDIA Tesla V100 GPU.

ComponentPartSize
Client libraryhistory recording620 lines of Java
Client librarydatabase adapters900 lines of Java
Verifierdata structures and algorithms2k lines of Java
VerifierGPU optimizations550 lines of CUDA/C++
Verifierhistory parser and others1.2k lines of Java

5. Should your team care?

(from §2.1, §5, §6 setup, §8; the checklist framing is mine)

Matches the paper's setting if...

  • Your service talks to the database as a client you control (application servers are the "clients", §2.1).
  • You can run collectors and a verifier in your own trust domain, and capture the full request/response history.
  • Your transactions are, or can be translated into, key-value reads and writes. In the PostgreSQL experiments, Cobra translated SQL queries into key-value operations.
  • Client threads use blocking requests, one transaction at a time per session.
  • Your workload is read-heavy or uses read-modify-write; the paper notes that reads dominate common OLTP workloads.

Questions to ask first

  • Do you use range queries, joins, or aggregates that would need rewriting into key-value operations?
  • Is any part of your data access async or event-driven (for example, a non-blocking driver)? [added for you]
  • Is your workload write-heavy with many blind writes (writes without a prior read of the same key)? That is Cobra's slow case.
  • Is your daily average load within the 2k txn/sec the paper sustained on its workloads?
  • Can you afford overhead in the range Figure 12 reports for your kind of database?

What you get, per the paper: detection, not prevention. On a violation, the verifier hands you a certificate naming the problematic transactions (§5). The authors also note that Cobra could be used as the checker inside a testing framework such as Jepsen (§8).

Limitations (from the paper)

(from §1, §2.1, §3.5, §6.1–§6.2, §8)
  1. L1. No time guarantee. There is no guarantee Cobra terminates in reasonable time; the worst case is in principle exponential (the problem is NP-complete). The experiments on real workloads did terminate.
  2. L2. Key-value API only. No range queries or SQL operations such as join and sum, unless translated into key-value operations.
  3. L3. No async clients. Clients may be multithreaded but not async/event-driven.
  4. L4. Verifier and collectors must not crash. Their fault tolerance is left to future work.
  5. L5. No new bugs found in the wild. In the authors' words: "we lack a sensational headline."
  6. L6. Slow on many blind writes. Cobra can be slow on workloads with many unconstrained (blind) writes (BlindW-WM). On BlindW-RW and BlindW-WM, history eventually exceeds GPU memory in the scaling experiment.
  7. L7. Detects, does not prevent. For strict serializability it assumes collector clocks differ by less than a threshold (100 ms by default); if they differ by more, it may falsely reject a serializable history.
  8. L8. One trust domain, full history. Clients, collectors, and verifier must sit in one trust domain, and the verifier needs the full history of requests and responses.

After publication: not part of the paper

Snapshot dated 2026-09-30, reproduced as given in the source file. It was compiled by the blog's editor (an AI assistant working for the first author) and checked against each paper's own page; it is not peer-reviewed and was not written by the paper's authors. I did not browse the web for this version, so it may be out of date.

Who cites this paper

Work that builds on it

Each relation is stated as that paper reports it.

WorkVenueRelation to Cobra (as reported by that work)
PolySI (Huang, Liu, Chen, Wei, Basin, Li, Pan)PVLDB 2023Carries the polygraph-plus-MonoSAT approach from serializability to snapshot isolation; compares against a Cobra-based SI checker.
Viper (Zhang, Ji, Mu, Tan)EuroSys 2023Snapshot-isolation checker that integrates Cobra's combining-writes and coalescing-constraints optimizations.
Plume (Liu, Gu, Wei, Basin)OOPSLA 2024Sound and complete checking of weak isolation levels; compares against Cobra.
Emme / King Cobra (Clark, Rigger, Wickerson, Donaldson)EuroSys 2024; TOCS 2026Recovers version certificates from the database; the TOCS extension builds "King Cobra," a modified Cobra that can use version order.
AWDIT (Møldrup, Pavlogiannis)PLDI 2025Optimal-complexity weak-isolation tester; generates evaluation histories with Cobra's benchmarks.
Chronos / Aion (Li, Wei, Ouyang, Chen, Yang, Zhang, Pan)ICDE 2025Online checking from database timestamps; does not need Cobra's fence transactions.
MTC (Wei, Xiao, Yang, Liu, Yin, Chen, Pan)ICDE 2025Restricts 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 2026Drops the unique-value assumption; reports up to 4.4× over GPU-accelerated Cobra.
Boomslang (Tan, Zhang, Mu)arXiv 2026A 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

Ledger items included

Ledger items omitted

Additions I made