Babashka vs cream

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.

Load time

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
requirewhole process
bb55ms0.06s
JVM610ms0.93s
cream1310ms1.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

Performance of cream is better than bb for hot JIT-ed code, but worse than a JVM.

bbcreamJVM
malli validate, 1M calls300ms70ms35ms
fib 35, hot1230ms815ms73ms
fib 27, cold26ms46ms5ms
(reduce + (map inc (range 10000000)))157ms725ms80ms

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.

Binary size and build time

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.

Combining SCI and Crema

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:

  • Binary size: this hybrid is what goes from 75MB to about 200MB, for every user, even for scripts that never load a class. A class loaded from a jar calls JDK methods by name. In a native image those names are normally gone, and methods bb doesn't use are not compiled in at all. So Crema has to keep a table of all compiled classes and methods, and whole JDK packages have to be preserved.
  • Build time: compile times go up as well, as described above.

If GraalVM shrinks that table or turns the abort into an exception, I'll look at this again.

What bb gives up

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.

Conclusion

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

Tagged: clojure babashka graalvm

Babashka vs cream

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.

Load time

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
requirewhole process
bb55ms0.06s
JVM610ms0.93s
cream1310ms1.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

Performance of cream is better than bb for hot JIT-ed code, but worse than a JVM.

bbcreamJVM
malli validate, 1M calls300ms70ms35ms
fib 35, hot1230ms815ms73ms
fib 27, cold26ms46ms5ms
(reduce + (map inc (range 10000000)))157ms725ms80ms

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.

Binary size and build time

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.

What bb gives up

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.

Conclusion

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

Tagged: clojure babashka graalvm

OSS updates September and October 2026

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.

Sponsors

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.

gratitude

Current top tier sponsors:

Open the details section for more info about sponsoring.

Sponsor info

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!

Intro

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:

  • Automatic command line help and completions in babashka tasks via babashka.cli
  • Resolving dependencies without a JVM (by vendoring tools.deps and rewriting the maven part)
  • Babashka FFI: a foreign function library to call C-ABI functions from libraries
  • Better nREPL support: support popular nREPL middleware like CIDER's test middleware, the CIDER inspector, etc
  • TUI support via JLine

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!

Babashka workshop at Clojure/conj 2026, seen from the back of the roomRich Hickey and Rahul Dé getting his hamster hammock signed at Clojure/conj 2026

Highlights

Here are some personal favorite highlights from the last two months.

  • Squint

  • 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

  • Babashka.ffi

    • First Clojars release on 2026-09-18, for the JVM, native image, CLJS and nbb. Node.js 26.1+ through node:ffi
    • JVM performance PRs by @Ramblurr (#36, #37)
    • Adopted by sqlite4clj, which moved from coffi to babashka.ffi (#26, @Ramblurr)
  • Clj-kondo

    • Type system improvements: (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

Blog posts that I published in this period:

Updates

Here are some highlights per project. See each project's CHANGELOG.md for the full list.

  • Babashka: native, fast-starting Clojure interpreter for scripting.

    • Resolve dependencies without a JVM by default. Set :deps-resolver :jvm in bb.edn or the add-deps map, or BABASHKA_DEPS_RESOLVER=jvm, to use Java as before. See the blog post
    • Make clojure.tools.deps and its script namespaces, including clojure.tools.deps.script.make-classpath2, available to scripts
    • Bundle clojure.tools.build.api, with patched install and javac for babashka. A tools.build dependency on the classpath takes precedence and runs from source
    • Add bb 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.
    • Bind the nREPL server to 127.0.0.1 by default instead of 0.0.0.0. Use bb --nrepl-server 0.0.0.0:1667 in Docker
    • Bundle nREPL ops for the CIDER inspector, test runner (C-c C-t) and cider-version
    • Add the nREPL interrupt op. Interruption is cooperative and does not stop tight loops
    • Smaller binary: the bundled Clojure sources are stored gzipped, and jline's class files are no longer embedded a second time as resources.
    • Add the CIDER debugger to the nREPL server: #dbg, #break, C-u C-M-x and the step commands
    • Add CIDER's track-state to the nREPL server: font-lock of loaded vars and the box around instrumented functions
    • Add babashka.deps.maven with find-versions, with-repository-credentials, active-proxy and model-repos
    • Block the certificates of the eMudhra intermediate CA compromise (@stathissideris)
    • The macOS amd64 binary requires macOS 11.0 or later
    • Full changelog
  • babashka.ffi: call C functions in shared libraries from Clojure.

    • 0.1.1: First standalone release of babashka.ffi, available on Clojars for JVM Clojure, ClojureScript and nbb
    • Call C libraries from JVM Clojure on JDK 25 or newer, or from applications compiled with GraalVM 25 or newer. ClojureScript, nbb and shadow-cljs can call C libraries on Node.js 26.1 or newer through node:ffi
    • #63: callback accepts struct layouts as argument and return types
    • JVM call performance by @Ramblurr (#36, #37)
    • New examples: JavaScriptCore, pinball, SDL3, libusb and QuickJS
  • babashka.esbuild: use esbuild from babashka and the JVM

    • New project. Bundle and transform JavaScript, TypeScript, JSX and CSS through babashka.ffi, without Node.js
    • Expose esbuild's metafile and metafile analysis (#3, @lread)
    • Used by cljdoc and hyper
  • deps-deploy: deploy jars to Clojars and Maven Central from babashka

    • New project. Takes the options of slipset/deps-deploy, with transfers through babashka.http-client
    • GPG signing, local install, snapshots and several jars in one deploy
    • Maven Central through the Publisher Portal
  • SCI: Configurable Clojure/Script interpreter suitable for scripting

    • Clojure 1.13 map destructuring as of clojure/clojure 33fb064d (1.13.0-alpha11): :req!, :excess and :missing directives, :defaults removed. Adds the selector macro to clojure.core.
    • Speed up 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 60x
    • Fix #1094: a call to a multi-arity fn created a new fn for the called arity on every call, 25x slower than a single-arity call in CLJS
    • CLJS: creating a closure inside a jitted fn is 4x to 9x faster, and creating a multi-arity fn compiles instead of escaping to the interpreter
    • Fix instance? on a protocol returning true for a type the protocol is extended to, such as nil or Object, on the JVM
    • Full changelog
  • clj-kondo: static analyzer and linter for Clojure code that sparks joy.

    • NEW linter :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"}.
    • Catch up with Clojure 1.13.0-alpha9 destructuring
    • Type checker: a type predicate narrows the type of a local in if-not, cond and and. E.g. (when (and (string? x) (pos? n)) (inc x)) will warn.
    • Type checker: merge and merge-with require a map or nil as first map argument. E.g. (merge [1 2] [3 4]) will warn.
    • #2774: :warn-on-reflection also warns on Java constructor calls. E.g. (Date. x) will warn.
    • #3006, #3014, #3015, #3016: fixes for hooks generated with :clj-kondo/macroexpand-hook: referred macros, helper namespaces, requires without alias and nested hooks (@imrekoszo)
    • #2945: support :order-by, :having and :timeout in Datalevin and Datahike queries
    • #2998: analyze Java 25 class files (@skylightis666)
    • Full changelog
  • Squint: CLJS syntax to JS compiler

    • #453: Add source maps for compiled files
    • #594: support for await in doseq, with ^:await on a binding, (doseq [^:await x xs] ...), or on the binding vector for every binding (#1053, @shaunlebron)
    • Add eduction, implements? and keyword-identical?
    • Optimize compiled output for or, and and reify
    • Optimize compiled output for core predicates such as even? and string? in if, and, or and not
    • #1032: #html: escape attribute values from {:& m} (@mskyan)
    • Playground: Implement draggable resizing of editor and compile panes (#1045, @paintparty)
    • Playground: Support codemirror dark theme across editor, repl output, and compiled code instances (#1045, @paintparty)
    • Full changelog
  • neil: A CLI to add common aliases and features to deps.edn-based projects

    • 0.3.71: neil requires babashka 1.13.229 or later
    • #254: auto-generated CLI help and shell completions via babashka.cli. neil dep --help no longer throws
    • #272: make babashka.neil/-main easier to call from code (@lread)
    • #268: neil dep upgrade LIB upgrades only LIB instead of all deps
    • #267: neil dep add, dep upgrade and dep versions find versions in the :mvn/repos of deps.edn. dep versions no longer lists snapshots
  • babashka CLI: Turn Clojure functions into CLIs!

    • #226: use metadata and docstrings from :fn and :exec-fn vars in babashka and ClojureDart
    • dispatch: add :supplied to the opts map's :org.babashka/cli metadata with the set of option keys given on the command line
    • #233: Generated help no longer prints extra spaces when :prog is not specified (@lread)
    • Full changelog
  • nbb: Scripting in Clojure on Node.js using SCI

    • Add babashka.ffi built-in library for calling C functions. Requires Node.js 26.1 or newer
    • Bump SCI: faster multi-arity function dispatch, bumps edamame to 1.6.44
  • Edamame: configurable EDN and Clojure parser with location metadata and more

    • Performance: speed up parsing plain decimal numbers on the JVM #155
    • Performance: build map literals with transients #159
    • Require Clojure 1.10 or newer #158
  • deps.clj: a faithful port of the Clojure CLI Bash script to Clojure

    • Allow --version, --help, -Sdescribe and a cached -Spath to run without Java.
    • Add *make-classpath-fn* to allow in-process classpath resolution.
    • Catch up with Clojure CLI 1.12.6.1673
  • rewrite-edn: Utility lib on top of rewrite-clj

    • #39: Support namespaced maps like #:a{:x 1}
    • neil#239: Read and update maps with metadata like ^{:x 1} {:a 1}
    • Fix get, assoc, update and dissoc when a map value equals the key
  • http-client: HTTP client for Clojure and babashka

    • #86: uri-with-query no longer decodes the existing query, fragment and user info when :query-params are used (@dylanpulver)
    • #91: decode-body passes a nil body through, e.g. of a 407 to CONNECT
  • fs: file system utility library for Clojure

    • #284: non-recursive match and glob now return immediate child directories, not only files
    • #269: match and glob no longer drop pattern text after a 2nd colon on Unix (@marksto)
    • #279: minimum supported version of Clojure is now 1.11 (@lread)
  • html: HTML generation library inspired by squint's HTML tag

    • Omit attributes whose value is nil, or false in html mode: [:input {:disabled false}] rendered disabled="false", which disables the input. Matches hiccup (#19, @telekid)
    • Render true and false as "true" and "false" for aria-*, data-*, contenteditable, draggable, focusable and spellcheck. Names match in any case, so :spellCheck works as well
  • quickblog: lightweight static blog engine for Clojure and babashka

    • Add :pre-process-markdown-fn and :post-process-html-fn options to transform each post's Markdown and HTML (README) (@jmglov)
  • gh-release-artifact: Upload artifacts to GitHub releases idempotently

    • Throw when Github does not accept an upload. A failed upload no longer leaves a green build
    • Upload the new bytes before touching the existing asset when replacing one
  • Buzz: write a web application with the JVM or babashka only

    • New observe, atom-source and buzz.source/Source API. :watch and invalidate! are gone (#20)
  • raylib-bb-ffi: raylib bindings for babashka via babashka.ffi

    • New experiment, ported with an LLM. Tests whether the examples of raylib-jlt and raylib-clj run in babashka and on the JVM through babashka.ffi
  • cljbang: a Clojure-like language that runs as Emacs Lisp

    • Add for (#12) and juxt (#9, @kpassapk)
    • Fix #10: str/join with a separator and an empty coll returns ""
  • Cherry: Experimental ClojureScript to ES6 module compiler

    • Bump squint: don't crash the nREPL server on a SyntaxError from eval (#208)
  • Cream: Clojure + GraalVM Crema native binary

    • Compare load times, tooling and GraalVM build with babashka (#7)
  • lein-clj-kondo and clj-kondo-bb: released alongside each clj-kondo release

Contributions to third party projects:

  • antq: antq runs on babashka (#316, #380) and is installable through bbin (#386)
  • hyper: compile h/expr against a served squint core, tree-shaken with esbuild. See the blog post. Serve source maps for defc components behind :source-map?
  • nextjournal/markdown: #71: render the link title in the default :link renderer
  • clojure-mode: bind Option+S to unwrap on macOS (#62)

Other projects

These are some other projects I'm involved with, but little to no activity happened in the past two months.

Click for more details

Published: 2026-10-30

Tagged: clojure oss updates

Archive