I've given a lot of thought to whether cream should affect babashka. Cream is based on Crema, a native-image compatible bytecode interpreter, basically a JVM inside of native-image.
What it concretely would mean to move babashka to Crema is that we could run virtually any Clojure program without changes. This is exciting.
Crema is an impressive piece of engineering. Native-image could not define classes at run time before, which is the reason SCI exists. With Crema, eval, require and Java libraries just work in a native binary, and cream starts as fast as babashka.
But it has serious downsides compared to using SCI.
The main downside is the load time of Clojure sources. Evaluating a fn in cream runs the Clojure compiler and emits a class, which just takes more work. Compilation accounts for about 1000ms of the load. Defining the 1670 classes takes the rest. SCI interprets the forms and skips both steps.
Here is the time to load malli 0.16.4 from source:
DEPS='{:deps {metosin/malli {:mvn/version "0.16.4"}}}'
CP=$(clojure -Spath -Sdeps "$DEPS")
E="(time (require 'malli.core 'malli.generator))"
bb -cp "$CP" -e "$E"
clojure -Sdeps "$DEPS" -M -e "$E"
./cream -Scp "$CP" -M -e "$E"
bb ## 0.06s
JVM ############################ 0.93s
cream ######################################## 1.34s
require | whole process | |
|---|---|---|
| bb | 55ms | 0.06s |
| JVM | 610ms | 0.93s |
| cream | 1310ms | 1.34s |
Babashka on SCI is just way faster for loading bigger libraries from source and then executing some expressions, which is commonly called "scripting". When loading a bigger lib like malli, you might as well just use a normal JVM instead of cream.
The gap shrinks with smaller libraries, but it stays quite significant. Babashka loads medley 9x faster than cream and camel-snake-kebab 15x faster.
Startup is the same in both babashka and cream: about 10ms.
Cream loads malli in about 330ms from AOT-compiled classes, and a cache could do that for you. But caches are a hassle, and babashka is faster than the AOT case anyway.
Performance of cream is better than bb for hot JIT-ed code, but worse than a JVM.
| bb | cream | JVM | |
|---|---|---|---|
| malli validate, 1M calls | 300ms | 70ms | 35ms |
fib 35, hot | 1230ms | 815ms | 73ms |
fib 27, cold | 26ms | 46ms | 5ms |
(reduce + (map inc (range 10000000))) | 157ms | 725ms | 80ms |
Cream's JIT makes no difference in the last row, it takes 725ms with the JIT off too.
Cream's JIT is still warming up during fib 27. For a script, the code does not run long enough for that JIT to pay off. Since the core feature of bb is scripting with good startup time, bb + SCI wins here.
Another major downside of including Crema is the binary size. Even when I only use Crema for loading Java classes, as described below, the binary goes up from 75MB to about 200MB. Cream itself is about 225MB. Maybe this isn't that important, but compile times also go up. I expect double or triple in CI, which isn't fun at all when you build a binary for several OSes. SCI is just way more light weight here too.
Another option is to combine both approaches: SCI keeps interpreting Clojure and Crema is only used to load Java classes that are not in the image. I tried this on the crema-hybrid branch and it works for many libraries. A tiny script with BouncyCastle, commons-compress or Caffeine takes about 40ms for the whole process, against about 320ms on the JVM. Loading malli is slow in cream because Clojure compiles every form and defines about 1670 classes. A Java library is already compiled, and a single call only loads the classes it needs.
Load time of Clojure code is not the problem here, since SCI still does that. The main downside of this hybrid is that a JDK package that wasn't preserved at build time aborts the process instead of throwing an exception, and you only find out by running the library. Of course we can protect users from this with extensive testing.
The two other downsides are the same as for cream:
If GraalVM shrinks that table or turns the abort into an exception, I'll look at this again.
Code that needs real JVM classes, or a Java library that bb doesn't ship, stays out of reach. Pods cover part of that. For the rest there is the JVM, or cream, which sits in the gray area between babashka and JVM Clojure.
This is not a verdict on Crema, which is still experimental and improving fast. A native binary with full Clojure and a working JIT is very interesting and exciting. Most of the load time comes from the Clojure compiler. It's just not the right trade-off for babashka today. Perhaps with an experimental fork of Clojure that reduces the number of emitted classes, this picture will change. I do use a fork of Clojure for cream, but it only has very mild changes to make it work with Crema. I haven't changed anything significant in the compiler.
The hybrid is more tempting, since it keeps SCI's load time and loads Java libraries faster than a JVM. But today it costs every user about 125MB and slower builds, and a library can still abort the process. So for now it stays on a branch.
For a while I thought SCI would be replaced with Crema in bb, but it still wins for the scripting use case. And babashka is all about scripting. I will keep improving SCI to make it both load and run faster over time. I was planning to do that anyway. Now that SCI isn't going away any time soon, I'm even more determined to make babashka the best possible scripting experience for Clojure.
You can view more numbers in the cream README.
Published: 2026-11-02
I've given a lot of thought to whether cream should affect babashka. Cream is based on Crema, a native-image compatible bytecode interpreter, basically a JVM inside of native-image.
What it concretely would mean to move babashka to Crema is that we could run virtually any Clojure program without changes. This is exciting.
Crema is an impressive piece of engineering. Native-image could not define classes at run time before, which is the reason SCI exists. With Crema, eval, require and Java libraries from jars just work in a native binary, and cream starts as fast as babashka.
But it has serious downsides compared to using SCI.
The main downside is the load time of Clojure sources. Evaluating a fn in cream runs the Clojure compiler and emits a class, which just takes more work. Compilation accounts for about 1000ms of the load. Defining the 1670 classes takes the rest. SCI interprets the forms and skips both steps.
Here is the time to load malli 0.16.4 from source:
DEPS='{:deps {metosin/malli {:mvn/version "0.16.4"}}}'
CP=$(clojure -Spath -Sdeps "$DEPS")
E="(time (require 'malli.core 'malli.generator))"
bb -cp "$CP" -e "$E"
clojure -Sdeps "$DEPS" -M -e "$E"
./cream -Scp "$CP" -M -e "$E"
bb ## 0.06s
JVM ############################ 0.93s
cream ######################################## 1.34s
require | whole process | |
|---|---|---|
| bb | 55ms | 0.06s |
| JVM | 610ms | 0.93s |
| cream | 1310ms | 1.34s |
Babashka on SCI is just way faster for loading bigger libraries from source and then executing some expressions, which is commonly called "scripting". When loading a bigger lib like malli, you might as well just use a normal JVM instead of cream.
The gap shrinks with smaller libraries, but it stays quite significant. Babashka loads medley 9x faster than cream and camel-snake-kebab 15x faster.
Startup is the same in both babashka and cream: about 10ms.
Cream loads malli in about 330ms from AOT-compiled classes, and a cache could do that for you. But caches are a hassle, and babashka is faster than the AOT case anyway.
Performance of cream is better than bb for hot JIT-ed code, but worse than a JVM.
| bb | cream | JVM | |
|---|---|---|---|
| malli validate, 1M calls | 300ms | 70ms | 35ms |
fib 35, hot | 1230ms | 815ms | 73ms |
fib 27, cold | 26ms | 46ms | 5ms |
(reduce + (map inc (range 10000000))) | 157ms | 725ms | 80ms |
Cream's JIT makes no difference in the last row, it takes 725ms with the JIT off too.
Cream's JIT is still warming up during fib 27. For a script, the code does not run long enough for that JIT to pay off. Since the core feature of bb is scripting with good startup time, bb + SCI wins here.
Another major downside of including Crema is the binary size. I built bb with Crema and the binary goes up from 75MB to about 200MB. Cream itself is about 225MB. Maybe this isn't that important, but compile times also go up. I expect double or triple in CI, which isn't fun at all when you build a binary for several OSes. SCI is just way more light weight here too.
Code that needs real JVM classes, or a Java library that bb doesn't ship, stays out of reach. Pods cover part of that. For the rest there is the JVM, or cream, which sits in the gray area between babashka and JVM Clojure.
This is not a verdict on Crema, which is still experimental and improving fast. A native binary with full Clojure and a working JIT is very interesting and exciting. Most of the load time comes from the Clojure compiler. It's just not the right trade-off for babashka today. Perhaps with an experimental fork of Clojure that reduces the number of emitted classes, this picture will change. I do use a fork of Clojure for cream, but it only has very mild changes to make it work with Crema. I haven't changed anything significant in the compiler.
For a while I thought SCI would be replaced with Crema in bb, but it still wins for the scripting use case. And babashka is all about scripting. I will keep improving SCI to make it both load and run faster over time. I was planning to do that anyway. Now that SCI isn't going away any time soon, I'm even more determined to make babashka the best possible scripting experience for Clojure.
You can view more numbers in the cream README.
Published: 2026-11-02
In this post I'll give updates about open source I worked on during September and October 2026.
To see previous OSS updates, go here.
I'd like to thank all the sponsors and contributors who make this work possible. Without you, the projects below would not be as mature or would not exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.

Current top tier sponsors:
Open the details section for more info about sponsoring.
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. If you work for a company that uses my OSS, please ask your employer, that would be even better. Thank you!
A lot happened again in the last two months! One major highlight was my visit to the Clojure Conj together with Rahul Dé, where we organized a Babashka workshop. It was great seeing lots of familiar and new faces over there. To give people at the workshop the best experience possible, lots of improvements went into babashka prior to the Conj... workshop driven development I guess. The major milestones I focused on during the last 6-8 months were:
All of this combined made for a great workshop, where we developed a stateless password manager that used cryptography either by shelling out or by FFI. This project combined the most-used libraries in babashka, like babashka.fs, babashka.process, babashka.cli, babashka.ffi, etc. so even if you didn't end up using this password manager (most people probably didn't) you certainly were exposed to what you would typically use in a bb script or project. Rahul and I will give this workshop at EuroClojure 2027 as well, so in case you missed it, you'll get one more chance!


Here are some personal favorite highlights from the last two months.
Antq, a tool to update outdated deps, now runs with bb!
You can now run build.clj in babashka with tools.build (which comes bundled with bb) and get --help and shell completions for your build tasks. Sean Corfield has been adopting this in projects he maintains. See for example: next.jdbc, HoneySQL, logging4j2 and rephrase by Sean Corfield
(when (and (string? x) (pos? n)) (inc x)) will now warn since (string? x) implies x is a string and (inc x) will crash on that input:unknown-map-key linter that warns when requesting keys from maps that provably aren't there: given (defn opts [] {:host "localhost"}), (:hots (opts)) will warn.Blog posts that I published in this period:
Here are some highlights per project. See each project's CHANGELOG.md for the full list.
Babashka: native, fast-starting Clojure interpreter for scripting.
:deps-resolver :jvm in bb.edn or the add-deps map, or BABASHKA_DEPS_RESOLVER=jvm, to use Java as before. See the blog postclojure.tools.deps and its script namespaces, including clojure.tools.deps.script.make-classpath2, available to scriptsclojure.tools.build.api, with patched install and javac for babashka. A tools.build dependency on the classpath takes precedence and runs from sourcebb repl --connect [addr] to connect to an nREPL server with completion, eldoc, documentation lookup and Ctrl-C interruption. Accepts host:port, a port or unix://path. Reads .nrepl-port by default.127.0.0.1 by default instead of 0.0.0.0. Use bb --nrepl-server 0.0.0.0:1667 in DockerC-c C-t) and cider-versioninterrupt op. Interruption is cooperative and does not stop tight loops#dbg, #break, C-u C-M-x and the step commandsbabashka.deps.maven with find-versions, with-repository-credentials, active-proxy and model-reposbabashka.ffi: call C functions in shared libraries from Clojure.
babashka.ffi, available on Clojars for JVM Clojure, ClojureScript and nbbnode:fficallback accepts struct layouts as argument and return typesbabashka.esbuild: use esbuild from babashka and the JVM
deps-deploy: deploy jars to Clojars and Maven Central from babashka
babashka.http-clientSCI: Configurable Clojure/Script interpreter suitable for scripting
:req!, :excess and :missing directives, :defaults removed. Adds the selector macro to clojure.core.binding (about 4x on the JVM, 2x in native bb) and try without a catch (about 4x)ns-map is 6x faster and ns-refers 60xinstance? on a protocol returning true for a type the protocol is extended to, such as nil or Object, on the JVMclj-kondo: static analyzer and linter for Clojure code that sparks joy.
:unknown-map-key: warn on a keyword lookup of a key not present in a map. E.g. (:hots (opts)) will warn when opts returns {:host "localhost"}.if-not, cond and and. E.g. (when (and (string? x) (pos? n)) (inc x)) will warn.merge and merge-with require a map or nil as first map argument. E.g. (merge [1 2] [3 4]) will warn.:warn-on-reflection also warns on Java constructor calls. E.g. (Date. x) will warn.:clj-kondo/macroexpand-hook: referred macros, helper namespaces, requires without alias and nested hooks (@imrekoszo):order-by, :having and :timeout in Datalevin and Datahike queriesSquint: CLJS syntax to JS compiler
for await in doseq, with ^:await on a binding, (doseq [^:await x xs] ...), or on the binding vector for every binding (#1053, @shaunlebron)eduction, implements? and keyword-identical?or, and and reifyeven? and string? in if, and, or and not#html: escape attribute values from {:& m} (@mskyan)neil: A CLI to add common aliases and features to deps.edn-based projects
neil dep --help no longer throwsbabashka.neil/-main easier to call from code (@lread)neil dep upgrade LIB upgrades only LIB instead of all depsneil dep add, dep upgrade and dep versions find versions in the :mvn/repos of deps.edn. dep versions no longer lists snapshotsbabashka CLI: Turn Clojure functions into CLIs!
:fn and :exec-fn vars in babashka and ClojureDartdispatch: add :supplied to the opts map's :org.babashka/cli metadata with the set of option keys given on the command line:prog is not specified (@lread)nbb: Scripting in Clojure on Node.js using SCI
babashka.ffi built-in library for calling C functions. Requires Node.js 26.1 or newer1.6.44Edamame: configurable EDN and Clojure parser with location metadata and more
deps.clj: a faithful port of the Clojure CLI Bash script to Clojure
--version, --help, -Sdescribe and a cached -Spath to run without Java.*make-classpath-fn* to allow in-process classpath resolution.rewrite-edn: Utility lib on top of rewrite-clj
http-client: HTTP client for Clojure and babashka
uri-with-query no longer decodes the existing query, fragment and user info when :query-params are used (@dylanpulver)decode-body passes a nil body through, e.g. of a 407 to CONNECTfs: file system utility library for Clojure
html: HTML generation library inspired by squint's HTML tag
nil, or false in html mode: [:input {:disabled false}] rendered disabled="false", which disables the input. Matches hiccup (#19, @telekid)true and false as "true" and "false" for aria-*, data-*, contenteditable, draggable, focusable and spellcheck. Names match in any case, so :spellCheck works as wellquickblog: lightweight static blog engine for Clojure and babashka
gh-release-artifact: Upload artifacts to GitHub releases idempotently
Buzz: write a web application with the JVM or babashka only
observe, atom-source and buzz.source/Source API. :watch and invalidate! are gone (#20)raylib-bb-ffi: raylib bindings for babashka via babashka.ffi
babashka.fficljbang: a Clojure-like language that runs as Emacs Lisp
Cherry: Experimental ClojureScript to ES6 module compiler
Cream: Clojure + GraalVM Crema native binary
lein-clj-kondo and clj-kondo-bb: released alongside each clj-kondo release
Contributions to third party projects:
h/expr against a served squint core, tree-shaken with esbuild. See the blog post. Serve source maps for defc components behind :source-map?:link rendererThese are some other projects I'm involved with, but little to no activity happened in the past two months.
Published: 2026-10-30
Tagged: clojure oss updates