kuilt Help

GCounter

A counter that only goes up. Each device owns its own slot; the total is the sum across all devices. On merge each slot keeps whichever value is larger — so the same increment seen from two devices is never double-counted.

Converges to: the total number of increments applied across all replicas, with no replica able to affect another's slot.

Merge rule

Each replica r owns slot r. inc(r, n) raises slot r by n. piece(a, b) takes element-wise max of every slot:

piece({A:2, B:1}, {A:1, B:3}) = {A:2, B:3} // max per slot, not sum

This is why merging doesn't double-count: the same increment, seen from two replicas, merges idempotently.

Code examples

Zero counter and summing across replicas:

@Test fun zeroHasValueZero() { assertEquals(0L, GCounter.ZERO.value) }

Inc produces a delta:

@Test fun incProducesADeltaThatRaisesTheCount() { val gc = GCounter.ZERO val delta = gc.inc(a, 3L) val next = gc.piece(delta) assertEquals(3L, next.value) assertEquals(3L, next.count(a)) }

Merge is element-wise max, not sum:

@Test fun pieceTakesElementwiseMaxNotSum() { // each replica owns its own slot; merge is max, so concurrent bumps to // DIFFERENT slots both count, but the same slot does not double-count assertEquals( GCounter.of(a to 2L, b to 3L), GCounter.of(a to 2L, b to 1L).piece(GCounter.of(a to 1L, b to 3L)), ) }

JSON round-trip:

@Test fun roundTripsThroughJson() { val gc = GCounter.of(a to 2L, b to 5L) val encoded = Json.encodeToString(GCounter.serializer(), gc) assertEquals(gc, Json.decodeFromString(GCounter.serializer(), encoded)) }

When to use

Use GCounter when you only need to count up — events fired, messages sent, operations completed. For a counter that can also go down, see PNCounter. For a counter with a shared budget that must never be overdrawn, see BoundedCounter.

Last modified: 22 June 2026