Browse Learn Clojure Foundations as a Java Developer

Benchmark and Profile Concurrent Clojure

Learn a practical JVM measurement workflow for Clojure concurrency: isolate pure code, run realistic load, inspect queues and threads, and avoid misleading microbenchmarks.

Benchmarking concurrent Clojure code has the same trap as benchmarking Java concurrency: measuring a tiny expression rarely tells you how the system behaves under real load. You need both microbenchmarks for focused functions and profiling for the full runtime shape.

Use the same discipline you would use on the JVM in Java: warm up, repeat, isolate variables, compare like with like, and verify the result in a profiler.

Choose The Right Measurement

Question Measurement tool
Is this pure function allocating too much? Criterium or another focused benchmark.
Is shared state contended? Load test plus retry/queue metrics.
Are threads blocked? JFR, VisualVM, YourKit, or thread dumps.
Is GC affecting latency? JVM GC logs, JFR allocation events, or profiler allocation views.
Is Java interop expensive? Profile the hot path and inspect reflection/boxing/conversion.

Do not ask one benchmark to answer all of these questions.

Benchmark Pure Work First

Pure functions are easiest to benchmark because they do not retain state between runs.

 1(require '[criterium.core :as crit])
 2
 3(defn summarize-events
 4  [events]
 5  (reduce (fn [summary {:keys [type]}]
 6            (update summary type (fnil inc 0)))
 7          {}
 8          events))
 9
10(def sample-events
11  (vec (for [n (range 10000)]
12         {:id n
13          :type (if (even? n) :order/created :order/updated)})))
14
15(crit/quick-bench
16  (summarize-events sample-events))

This answers a narrow question: how expensive is the local transformation? It does not answer whether atom contention, database writes, or executor saturation will hurt production throughput.

Measure A Real Concurrent Workload Separately

When testing coordination, include the coordination.

 1(defn run-counter-load!
 2  [worker-count updates-per-worker]
 3  (let [counter (atom 0)
 4        workers (doall
 5                  (for [_ (range worker-count)]
 6                    (future
 7                      (dotimes [_ updates-per-worker]
 8                        (swap! counter inc)))))]
 9    (doseq [worker workers]
10      @worker)
11    @counter))
12
13(run-counter-load! 8 10000)
14;; => 80000

This is a coarse load probe, not a perfect benchmark. It is useful because it exercises futures, scheduling, atom contention, and joins together. If this number looks suspicious, move to a proper load harness and profiler.

Profile The JVM, Not Just The Clojure Source

Most useful Clojure performance evidence still appears as JVM evidence.

Profiler signal Clojure interpretation
Many threads parked or blocked The bottleneck may be I/O, executor capacity, or queue backpressure.
Hot clojure.lang collection operations Review allocation, lazy realization, and intermediate collections.
High time in atom or ref update paths Check contention, transaction size, and update frequency.
Reflection warnings or reflective calls Add type hints or move interop out of the inner loop.
High allocation in sequence processing Consider eager realization, reducers, transients, or chunking after profiling.

Run with realistic data shape. A benchmark over ten tiny maps can miss costs that appear only with real payloads, nested values, or Java object conversion.

Make Comparisons Fair

When comparing a Clojure implementation to Java code, hold the engineering variables steady.

Variable Keep consistent
JDK Same version, same JVM flags, same hardware.
Workload Same data size, blocking behavior, and success/error mix.
Warm-up Enough iterations for JIT compilation and stable allocation behavior.
State model Compare equivalent consistency guarantees, not atom updates against unsafe mutation.
Output Verify both versions produce the same result under concurrency.

If the Java version mutates unsafely and the Clojure version preserves correctness, the speed comparison is not meaningful.

Benchmarking Review Checklist

Check Failure mode avoided
Separate pure-function benchmarks from load tests. Confusing local CPU cost with system throughput.
Recreate state for each benchmark sample when needed. State leaking between runs.
Avoid printing or logging in measured loops. Measuring console I/O instead of program logic.
Use a profiler after a surprising benchmark. Optimizing the wrong layer.
Record the JDK, flags, data shape, and hardware. Results that cannot be reproduced.

Knowledge Check

### Why benchmark pure functions separately from concurrent load tests? - [x] Pure benchmarks isolate local computation, while load tests expose scheduling and contention. - [ ] Pure functions cannot be profiled by JVM tools. - [ ] Clojure cannot benchmark functions that return maps. - [ ] Concurrent load tests automatically remove GC overhead. > **Explanation:** A pure benchmark answers a narrow CPU/allocation question. A load test includes queues, thread scheduling, retries, blocking, and contention. ### What should you do after a surprising benchmark result? - [x] Validate it with profiling and a reproducible workload description. - [ ] Immediately replace all atoms with refs. - [ ] Disable the JVM warm-up period. - [ ] Compare it against unrelated Java code. > **Explanation:** Surprising results need evidence. Profilers and reproducible workloads help prevent premature or wrong optimization. ### What makes a Java-vs-Clojure performance comparison unfair? - [x] Comparing unsafe Java mutation with correct Clojure coordination. - [ ] Running both versions on the same JDK. - [ ] Using the same input data. - [ ] Checking that outputs match. > **Explanation:** Performance comparisons must preserve correctness requirements. Faster incorrect code is not an optimization.
Revised on Saturday, May 23, 2026