Tufte: form-level performance monitoring for Clojure and ClojureScript
Simple performance monitoring library for Clojure/Script
At a glance
- What is it?
- Tufte is a small Clojure library that measures the performance of individual forms in a running application and hands the results back as plain Clojure data. It is a monitoring tool, not a profiler, it is meant to run in production, and it was last pushed to on 2026-07-30 with release v3.1.0.
- Who is it for?
- Adopt Tufte if you run a Clojure or ClojureScript service and want runtime measurements of specific forms, in production, delivered as data you can aggregate and store rather than as a flame graph. Do not adopt it to find where your code is slow, because it is built for monitoring selected forms rather than for profiling an entire application, and do not expect the Java or JavaScript ecosystems' profilers to be replaced by it.
- Can I use it commercially?
- Yes, with conditions. EPL-1.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 85 days ago.
- What is it written in?
- Mainly Clojure, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Monitoring chosen forms instead of profiling everything
Tufte is a performance monitoring library for Clojure and ClojureScript. The distinction the project draws is between monitoring and profiling, and the README is explicit about which side of that line it sits on. It allows you to easily monitor the ongoing performance of your Clojure and ClojureScript applications in production and other environments, and it provides sensible application-level metrics.
The mechanism is deliberately narrow. You mark the forms you care about, and the library reports how long those forms took, how often they were called, and how that time relates to the total clock time of the surrounding operation. The README describes this as form-level profiling without the low-level JVM noise, which is the right characterisation for a JVM tool: you are not reading a flame graph of every frame, you are getting a per-form table of call counts, minimum, percentile and maximum durations, mean, and a clock-to-total ratio.
That shape is what makes it usable in production. A full profiler changes what it measures and can slow the thing it measures. A monitoring library that samples named forms gives you a stable metric series you can alert on, aggregate and compare between releases, and it works the same way in ClojureScript, which is where most of the alternatives in the JavaScript ecosystem stop.
The audience is a Clojure developer who already knows which parts of a request path matter and wants numbers on them.
p, profiled and profile as the whole public surface
The API is described as tiny and flexible, with three entry points: p, profiled and profile. The quick example shows how they relate to each other in practice, and the shape of the library is visible in a single block of code:
(require '[taoensso.tufte :as tufte])
;; Send `profile` signals to console
(tufte/add-handler! :my-console-handler (tufte/handler:console))
;;; Define a couple dummy fns to simulate doing some expensive work
(defn get-x [] (Thread/sleep 500) "x val")
(defn get-y [] (Thread/sleep (rand-int 1000)) "y val")
;; Let's check how these fns perform:
(tufte/profile ; Profile any `p` forms called during body execution
{:level :info} ; Rich set of filtering options available
(dotimes [_ 5]
(tufte/p :get-x (get-x))
(tufte/p :get-y (get-y))))The layering is: p marks a single form with an id, profile wraps a body of code and reports on every p form called during it, and profiled is the third entry point in the same family. The profile call takes a map of options, here the level set to :info, and the README notes that a rich set of filtering options is available under that map.
The ids are the key. The p form takes a keyword such as :get-x, and that keyword becomes the row label in the report, which is what lets you chart a metric over time instead of reading a one-off table.
The example also shows the console handler, registered with add-handler! under a name, which is how results reach a destination. Handlers are covered separately, and they are the extension point that decides where your data goes.
Metrics as Clojure maps, not as a rendered chart
The second design decision is that the results are data, not a picture. The README says it provides sensible application-level metrics and gives them to you as Clojure data that can be easily analyzed programatically. Metrics as Clojure maps, as the feature list puts it, means easily aggregate, analyse, log, serialize to db.
That is the difference between this and a console table that you read once. If the console handler prints a table, that table is a rendering of the underlying map, and the map is what your own code receives. You can hand it to a metrics backend, put it in a log line, or keep it in memory and flush it on your own schedule.
The sample output in the README shows what one of those renderings looks like, and the columns are worth noting because they define the data model. Each row carries a pId, an nCalls count, then Min, 50%, 90%, 95% and 99% percentiles, Max and Mean, then MAD, then Clock and Total. The percentiles are the useful part for monitoring, since a mean hides the tail, and the Clock versus Total columns show how much of the profiled time the measured forms accounted for. The example output ends with an Accounted line and a Clock line, both at 100% of the 5.60s total.
MAD is the median absolute deviation, which is the same spread-by-median idea applied to a distribution that is skewed by occasional slow calls, and it is what lets you see that one function has a wildly variable duration while another is essentially constant. In the sample output, :get-y shows a plus or minus 42% spread against :get-x at plus or minus 0%.
Filtering by namespace, id pattern and level
Instrumenting an application in production is only viable if you can turn most of it off. Tufte's answer is rich filtering, described as conditional profiling by namespace, id pattern, level, level by namespace pattern.
That list matters more than it first appears. Filtering by namespace lets you keep a whole subsystem instrumented and quiet everywhere else. Filtering by id pattern lets you turn on a family of ids with a prefix and leave the rest silent. The level option, visible in the quick example as :info, is a per-call threshold, and the level-by-namespace-pattern variant lets you set the threshold differently for different parts of the codebase.
The practical effect is a tiered instrumentation model. You can leave a small number of ids always on, enable a namespace in a canary environment, or raise the level in one area when you are chasing a problem and lower it elsewhere to keep the overhead down. Without that, the cost of every p form in the codebase adds up and the monitoring becomes the incident.
There is also a support for thread-local and dynamic multi-threaded profiling, which is the other half of making this usable in a server. A web application handles requests on many threads, and a measurement that only captures the thread that set it up would report a fraction of the real work. The dynamic variant covers the multi-threaded case, and the thread-local one covers scoped measurement where you want the numbers attached to a specific flow rather than pooled across the process.
Handlers, sampling and back-pressure on the output path
Once a measurement exists, something has to receive it, and that is where the handling options live. The README lists rich async and sync handling, with sampling, rate limiting, back-pressure monitoring, and it ships handlers for Telemere, Timbre and consoles, where the console handler writes to *out* and similar streams.
The three named handlers tell you the intended integration shape. Telemere and Timbre are logging and telemetry tools in the same author's suite, and the README positions Tufte as one member of a set of complementary observability tools for modern Clojure and ClojureScript applications, alongside Telemere for logging and tracing, Truss for assertions and error handling, and Trove for library authors who want structured logging. If your application already logs through one of those, routing performance signals into the same pipeline is the low-friction path, and the console handler is there for a local check.
Sampling, rate limiting and back-pressure are the terms that matter for production use. A measurement system that can back up is a measurement system that eventually takes down the application it observes, so a back-pressure-aware handling path is what separates a monitoring library you can leave switched on from one you sample by hand.
The README does not document the default sampling rate or the default rate limit values, so anyone relying on a specific throughput has to read the API reference rather than guess. The API reference is on cljdoc, and the wiki covers getting started and usage.
Clojure, ClojureScript and Graal, with an EPL 1.0 licence
The project is a cross-platform Clojure and ClojureScript codebase, and the badge row at the top of the README shows three separate test workflows: Clj tests, Cljs tests and Graal tests. Three CI targets for a small library is a signal about how seriously the cross-platform claim is taken, and the Graal target in particular matters for anyone deploying to a native image, where reflection-based instrumentation tends to break.
The repository layout backs the same picture. Alongside src and test there is a bb directory and a bb.edn file, which is the Babashka build tool, plus an examples directory with clj and cljs subdirectories and a README.cljc, meaning the examples are written to run on both platforms. A wiki directory holds the long-form documentation, and doc and resources are present as well.
On licensing, the README states it is licensed under EPL 1.0, the same as Clojure, with the copyright held by Peter Taoussanis and dated 2016-2025. That matters more than it sounds. EPL 1.0 is a file-level copyleft: linking Tufte into your application is fine, and the obligations kick in only if you modify Tufte itself, in which case your modified version has to be published under the same terms. It is not the AGPL-style viral licensing that would force you to open-source your whole service, and it is not permissive BSD either. For most Clojure shops the practical answer is that a dependency on Tufte in a proprietary service creates no disclosure obligation, because you are not modifying Tufte.
The release line is v3.1.0 from 2026-07-30, with v3.0.2 in January 2026 and v3.0.1 earlier that month, and the last push was on 2026-07-30. The major version reached three during 2026, so the API has changed shape across major versions and the changelog in the repository root is where you look before upgrading from a v2 dependency.
Editorial conclusion
Adopt Tufte if you run a Clojure or ClojureScript service and want runtime measurements of specific forms, in production, delivered as data you can aggregate and store rather than as a flame graph. Do not adopt it to find where your code is slow, because it is built for monitoring selected forms rather than for profiling an entire application, and do not expect the Java or JavaScript ecosystems' profilers to be replaced by it. Verify first that the p, profiled and profile entry points cover the code you care about, that your handler choice can carry the result off the machine, and that adding instrumentation to hot paths is acceptable to you at the v3.1.0 API level.
Frequently asked questions
What does Tufte measure in a Clojure application?
Tufte monitors the performance of individual forms you mark, reporting call counts, minimum, percentile, maximum and mean durations, plus the share of clock time each one accounted for. It provides form-level profiling without the low-level JVM noise and returns the metrics as Clojure data.
How do I add Tufte to a Clojure project?
The library is published on Clojars as com.taoensso/tufte, and the latest release is v3.1.0 from 2026-07-30. In code you require it as taoensso.tufte and register a handler with add-handler!, for example the console handler.
What is the difference between p, profiled and profile in Tufte?
The API is described as tiny, with those three entry points. In the quick example, p marks a single form with an id such as :get-x, and profile wraps a body of code and reports on every p form called during that body, taking a map of filtering options.
Can Tufte be used in ClojureScript?
Yes. Tufte is described as a small, fast, cross-platform Clojure and ClojureScript codebase, with separate Clojure and ClojureScript test workflows in CI and examples written for both platforms. The project also runs its tests under Graal.
How can I limit the overhead of Tufte in production?
The README lists rich filtering for conditional profiling by namespace, id pattern, level and level by namespace pattern, and handling options with sampling, rate limiting and back-pressure monitoring. Handlers for Telemere, Timbre and consoles are included.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/taoensso-tufte)Was this useful?