# Conflict Free Counter An eventually-consistent, contention-free counter component for Bijection. - Package: @bijectionhq/conflict-free-counter - Category: Database & Backend - Status: Reference - License: Apache-2.0 - Bijection source (repository access required): https://github.com/bijectionhq/conflict-free-counter/tree/f95146c796529812ee01dbf2ad763230c4723cb9 - Upstream: https://github.com/stack1ng/convex-conflict-free-counter/tree/1b23f8137d13895105091c40a5835fc02f323144 - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/conflict-free-counter ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/conflict-free-counter.git cd conflict-free-counter git checkout f95146c796529812ee01dbf2ad763230c4723cb9 ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes convex-conflict-free-counter; check the Bijection adaptation separately. Conflict Free Counter provides an eventually-consistent, contention-free counter built on a conflict-free log architecture. Every call to `add` appends a row to an internal log rather than updating a shared document, eliminating write contention entirely. A scheduled compaction pipeline periodically folds the log into a snapshot, and reads return `snapshot + visible log entries` along with a `fullyConsistent` flag indicating whether the returned count reflects every committed write. ### Use cases - Tracking high-throughput event counts (page views, signups, API calls) where many concurrent mutations would otherwise cause document contention and retries - Incrementing and decrementing credit or quota balances across concurrent operations without serializing writes through a single document - Aggregating metrics or tallies in systems using database triggers, where a single mutation may update the same counter key many times and needs to batch those updates efficiently - Building dashboards or stats queries where reads can tolerate a few seconds of lag in exchange for writes that never block or retry - Replacing a manually sharded counter when you want throughput to scale automatically without choosing or rebalancing shard counts ### How it works Install the package and register it in `convex/convex.config.ts` using `app.use(conflictFreeCounter)`. Then instantiate `new ConflictFreeCounter(components.conflictFreeCounter)` once in a shared module and import that instance across your mutations and queries. Writes use `counter.add(ctx, key, delta)` or `counter.addMany(ctx, entries)` from any mutation or action. Each call appends to an internal log document rather than reading and updating a shared counter, so concurrent mutations never touch the same document and write conflicts are structurally impossible. For hot mutations that update the same key repeatedly, `counter.bindDeltasBuffer(ctx)` and `counter.addBuffered` coalesce increments in memory before a single `flushDeltas` call writes one log row per key. Reads use `counter.count(ctx, key)` which returns `{ count, fullyConsistent }`. By default, reads scan as much of the uncompacted log as the transaction's read budget allows on top of the latest snapshot. You can cap this with `logScanLimit` to reduce read cost, or set `logScanLimit: 0` to read the snapshot only, which is useful for reactive subscriptions that should update once per compaction rather than on every increment. Compaction runs on a scheduled background pipeline controlled by `compactionDelay` (default 15 seconds). After a write burst, reads may temporarily report `fullyConsistent: false` until compaction catches up, but no writes are ever lost. The package ships a `/test` entry point for use with `convex-test`, and fake timer support allows controlled compaction stepping in unit tests. ### Upstream README # Convex Conflict-Free Counter Component An **eventually-consistent, contention-free counter** for [Convex](https://convex.dev). Increment it from as many concurrent mutations as you like — writes never conflict with each other, so your mutations never slow down or retry because of counter contention. Use it for metrics, event counts, tallies, quotas — anywhere many writers bump the same counter and reads can tolerate a few seconds of lag (and every read tells you whether it lagged). **How it works**: every `add` appends a row to an internal log, which is conflict-free by construction. A scheduled compaction pipeline periodically folds the log into a per-key snapshot behind a lease, so the log stays small. Reads return `snapshot + visible log entries` along with a `fullyConsistent` flag — `true` when the returned count reflects every write committed before the read, `false` when the uncompacted log may have been larger than the scan budget, so the count may (temporarily) be missing recent increments. Compared to [`@convex-dev/sharded-counter`](https://www.convex.dev/components/sharded-counter): - No shard count to choose or rebalance — throughput scales automatically with write volume. - Reads are exact whenever the backlog fits in the read budget (and say so via `fullyConsistent`), instead of trading read contention against shard count. - The trade-off: after a burst of writes, a read may briefly lag behind by up to the compaction delay (default 15s). Writes are never lost — the count always converges. Found a bug? Feature request? [File it here](https://github.com/stack1ng/convex-conflict-free-counter/issues). ## Pre-requisite: Convex You'll need an existing Convex project to use the component. Convex is a hosted backend platform, including a database, serverless functions, and a ton more you can learn about [here](https://docs.convex.dev/get-started). Run `npm create convex` or follow any of the [quickstarts](https://docs.convex.dev/home) to set one up. ## Installation Install the component package: ```sh npm install convex-conflict-free-counter ``` Create a `convex.config.ts` file in your app's `convex/` folder and install the component by calling `use`: ```ts // convex/convex.config.ts import { defineApp } from "convex/server"; import conflictFreeCounter from "convex-conflict-free-counter/convex.config"; const app = defineApp(); app.use(conflictFreeCounter); export default app; ``` New to Convex components? See the [using components guide](https://docs.convex.dev/components/using) for how an installed component is wired into your app. Instantiate the client (typically in its own module so every function shares one instance): ```ts // convex/counter.ts import { ConflictFreeCounter } from "convex-conflict-free-counter"; import { components } from "./_generated/api"; export const counter = new ConflictFreeCounter(components.conflictFreeCounter); ``` ## Updating counters Add to (or subtract from) a counter from any mutation or action: ```ts // convex/events.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; import { counter } from "./counter"; export const recordEvent = mutation({ args: { kind: v.string() }, handler: async (ctx, { kind }) => { await counter.add(ctx, `events:${kind}`); // delta defaults to 1 await counter.add(ctx, "events:total", 1); await counter.add(ctx, "credits:remaining", -3); }, }); ``` Batch several updates into a single component call: ```ts await counter.addMany(ctx, [ { key: "views:home", delta: 1 }, { key: "views:total", delta: 1 }, ]); ``` ## Reading counters ```ts // convex/stats.ts import { query } from "./_generated/server"; import { v } from "convex/values"; import { counter } from "./counter"; export const eventCount = query({ args: { kind: v.string() }, handler: async (ctx, { kind }) => { const { count, fullyConsistent } = await counter.count( ctx, `events:${kind}`, ); // `fullyConsistent` is true when `count` reflects every write committed // before this query started. return { count, fullyConsistent }; }, }); ``` By default, reads scan as much of the uncompacted log as the transaction's read budget allows, so they are exact except during very large write bursts. You can bound (or skip) the log scan for cheaper reads: ```ts // Read at most 100 uncompacted log entries on top of the snapshot: await counter.count(ctx, key, { logScanLimit: 100 }); // Snapshot only — cheapest possible read, lags by up to the compaction delay: await counter.count(ctx, key, { logScanLimit: 0 }); ``` Note that a query that scans the log re-runs whenever the counter changes, as any reactive Convex query would. Use `logScanLimit: 0` for subscriptions that should only update once per compaction rather than on every increment. ## Configuration ```ts export const counter = new ConflictFreeCounter(components.conflictFreeCounter, { // Wait this long after a write before compacting (default: 15s). // Longer = fewer, larger compactions. Shorter = reads converge faster. compactionDelay: 15_000, // How long one compaction round may take before its lease can be stolen // (default: 60s). You shouldn't normally need to change this. compactionLeaseDuration: 60_000, // Default `logScanLimit` for count() (default: unlimited, i.e. scan // within the transaction's read budget). defaultLogScanLimit: undefined, }); ``` ## Buffering updates in hot mutations If one mutation updates the same keys many times (for example from [database triggers](https://stack.convex.dev/triggers)), you can buffer deltas in memory and flush them with one component call at the end: ```ts const bufferedCtx = counter.bindDeltasBuffer(ctx); await counter.addBuffered(bufferedCtx, "tasks:completed"); await counter.addBuffered(bufferedCtx, "tasks:completed"); // coalesced await counter.addBuffered(bufferedCtx, "tasks:pending", -2); await counter.flushDeltas(bufferedCtx); // one addMany call, 2 log rows ``` ## Consistency model - **Writes are never lost.** Every `add` is durably committed in your mutation's transaction. The count always converges to the true total. - **Reads are exact when `fullyConsistent` is `true`.** This is the common case: the snapshot plus the scanned log tail covers every committed write. - **Reads can lag when `fullyConsistent` is `false`.** This happens when more log entries exist than the read was willing (or able) to scan. The lag is bounded by the compaction cadence — roughly `compactionDelay` plus the time to drain the backlog. - **Counts are floats.** Deltas are Convex `number`s (IEEE 754 doubles), so integer counts stay exact up to 2^53. ## Testing your app The package ships a `/test` entry point so you can register the component in your own [`convex-test`](https://docs.convex.dev/functions/testing) suites — no running backend required. First expose your app's modules once, in a shared setup file: ```ts // convex/test.setup.ts /// export const modules = import.meta.glob("./**/*.ts"); ``` Then register the component on each test instance and drive its scheduled compaction with fake timers: ```ts import { convexTest } from "convex-test"; import { expect, test, vi } from "vitest"; import conflictFreeCounter from "convex-conflict-free-counter/test"; import schema from "./schema"; import { modules } from "./test.setup"; import { api } from "./_generated/api"; test("counts events", async () => { const t = convexTest(schema, modules); // Pass the name here if you mounted it with `app.use(counter, { name })`. conflictFreeCounter.register(t); await t.mutation(api.events.record, { kind: "signup" }); // Compaction runs through the scheduler. Advance timers one step at a // time so each lease-guarded compaction step runs in order (jumping the // whole timeline with `vi.runAllTimers` would expire leases early). vi.useFakeTimers(); await t.finishAllScheduledFunctions(() => vi.advanceTimersToNextTimer()); vi.useRealTimers(); expect(await t.query(api.events.count, { kind: "signup" })).toMatchObject({ count: 1, }); }); ``` `register(t, name?)` calls `t.registerComponent` under the hood; the default name is `conflictFreeCounter`. ## Running the example ```sh npm i npm run dev ``` See [example/convex/example.ts](https://github.com/stack1ng/convex-conflict-free-counter/blob/main/example/convex/example.ts) for a complete usage example.