Gcry: a garbage collector written in pure Crystal

I just released 0.14.0 of gcry. It’s a conservative mark sweep GC written entirely in Crystal, shipped as a shard. No C code, no compiler patches, no fork. You just add require "gcry" and build with -Dgc_none.


Performance

Same host, median of 3, wrk -c 100 -d 30:

Workload gcry vs Boehm Post-GC RSS
Kemal /json (Linux) ~89% throughput 0.79x Boehm
Kemal root (Linux) ~89% throughput 0.78x Boehm
Kemal /json (macOS) ~84% throughput 0.93x Boehm
Kemal root (macOS) ~93% throughput 1.06x Boehm

21% less memory than Boehm on Linux Kemal. Same throughput.


What makes gcry different

It’s a conservative, non-moving, stop-the-world collector. Same family as Boehm, so it works with Crystal’s existing ABI. But it’s written in Crystal, which means:

  • You can read it. The whole collector is about 9,000 lines of Crystal across 32 files. No C macros, no opaque foreign library.
  • You can debug it. Set a breakpoint anywhere. Crystal’s debugger works on it.
  • You can change it. Want to experiment with a different marking strategy? Edit a .cr file and rebuild.

Architecture: mmap-backed size classes, fiber stack roots, conservative mark with layout precise opt-in, deferred madvise, and platform specific STW (Linux signals, Mach thread_suspend on macOS).


What’s new in 0.14.0

This release is all about quality infrastructure:

  • Debug invariant checker (GCRY_DEBUG_INVARIANTS=1). Validates live_objects counter, freelist consistency, chunk index integrity, block overlap detection. At runtime, on every malloc/free/collect.
  • AddressSanitizer and Valgrind on every PR in CI.
  • Coverage reports via kcov on every PR.
  • Deterministic replay fuzzing with --seed and --replay.
  • 24 hour soak tests, thread storm tests, OOM tests, property based heap invariant tests.
  • Heap dump (Gcry.dump_heap) streams live objects as NDJSON for leak hunting.
  • Trace (GCRY_TRACE=1) traces every collect, malloc, and free.

How to try it

# shard.yml
dependencies:
  gcry:
    github: sdogruyol/gcry
{% if flag?(:gc_none) %}
  require "gcry"
{% end %}

puts "Hello from gcry!"
crystal build -Dgc_none app.cr -o app

No special malloc API. No compiler fork. String, Array, Hash, everything works as usual.


Limitations

gcry is a shard and I’m pushing it as far as I can. But there are things you can’t do from a shard. True precise stack scanning, write barriers, and concurrent collection need compiler support. At some point that line has to be crossed. But for now, the shard approach gets you surprisingly far: Kemal class HTTP at near Boehm perf, layout precise heap scanning, fiber stack scrubbing, and full observability. All without touching the compiler.


Github

Hi. gcry looks interesting. I’m curious how you handle parallelism. Does it work with 1.21 Execution Contexts?

Hi Kostya, thanks.

Yes: gcry targets Crystal ≥ 1.21 Execution Contexts. The supported path is the default EC at parallelism 1. STW suspends other OS threads (including the Monitor), and we scan fiber stacks from that world.

Extra parallel contexts are experimental. You can try them with GCRY_TLAB=1. We have CI covering Parallel workers under real process STW, but I wouldn’t call that production-ready yet. GCRY_PARALLEL_MARK=N is also experimental and often hurts HTTP throughput vs N=1, so measure before turning it on.

Short version: default EC / parallelism 1 is what we ship for. Parallel ECs work enough to test, not enough to promise.