- Status
- Reference implementation
- System requirements
- Usage
- API
- Benchmarks
- Documentation
- License
- Additional info
Common Lisp bindings for the brotli compression library, providing Gray-stream-based compression and decompression.
Status
Functional. Used as the brotli compression backend for datastar-cl. That said, not thoroughly tested.
Reference implementation
This library was inspired by cl-zstd by Guillaume Le Vaillant: the file structure, gray-stream class design, API naming, and test approach all follow cl-zstd as closely as possible.
This is intentional: cl-zstd is a clean, well-structured library and cl-brotli is meant to be readable alongside it, but even more relevant for this choice, cl-zstd was already supported for SSE streams, making it a good starting point: not being an expert in compression streams, having a reference implementation to follow was fundamental.
Where the brotli C API forces a deviation, the source code includes a short comment explaining why. Readers familiar with cl-zstd should be able to read cl-brotli with minimal friction. The actual implementation is completely different, being based on two different libraries (libbrotli vs libzstd).
System requirements
The brotli shared libraries must be installed:
# Debian/Ubuntu
apt install libbrotli1 libbrotli-dev
etc.
Usage
This is a library so usage will be done by applications; the following snippets show some of the common entry points:
(ql:quickload :cl-brotli)
;; One-shot
(let* ((data #(1 2 3 4 5))
(compressed (brotli:compress-buffer data))
(decompressed (brotli:decompress-buffer compressed)))
(equalp data decompressed)) ; => T
;; Streaming (encoder)
(with-open-file (out "/tmp/data.br" :direction :output :element-type '(unsigned-byte 8))
(brotli:with-compressing-stream (stream out :level 4)
(write-sequence (cl-octet-streams:string-to-octets "some-bytes") stream)))
;; Streaming (decoder)
(with-open-file (in "/tmp/data.br" :element-type '(unsigned-byte 8))
(brotli:with-decompressing-stream (stream in)
(read-sequence buffer stream)))
An Huchentoot minimal example is in examples/server.lisp:
$ sbcl --load examples/server.lisp
[...]
Serving on http://localhost:4242/hello (C-c to stop)
[...]
127.0.0.1 - [2026-06-03 20:25:45] "GET /hello HTTP/1.1" 200 - "-" "curl/8.20.0"
Using curl:
$ curl --compressed -v http://localhost:4242/hello
[...]
> GET /hello HTTP/1.1
> Host: localhost:4242
> User-Agent: curl/8.20.0
> Accept: */*
> Accept-Encoding: deflate, gzip, br, zstd
[...]
< Server: Hunchentoot 1.3.1
< Transfer-Encoding: chunked
< Vary: Accept-Encoding
< Content-Encoding: br
< Content-Type: text/plain; charset=utf-8
[...]
Hello, Brotli!
Compression levels
Brotli quality ranges from 0 (fastest) to 11 (best compression, slowest).
- Default in this library: 4. Fast, adequate for real-time streaming (e.g., SSE which was the initial use-case).
- Brotli's upstream default: 11. Designed for static asset pre-compression, usually too slow for per-event streaming.
API
The API mirrors cl-zstd closely; this table shows the initial mapping:
| cl-brotli | cl-zstd equivalent |
|---|---|
brotli:make-compressing-stream |
zstd:make-compressing-stream |
brotli:with-compressing-stream |
zstd:with-compressing-stream |
brotli:make-decompressing-stream |
zstd:make-decompressing-stream |
brotli:with-decompressing-stream |
zstd:with-decompressing-stream |
brotli:compress-buffer |
zstd:compress-buffer |
brotli:compress-stream |
zstd:compress-stream |
brotli:compress-file |
zstd:compress-file |
brotli:decompress-buffer |
zstd:decompress-buffer |
brotli:decompress-stream |
zstd:decompress-stream |
brotli:decompress-file |
zstd:decompress-file |
brotli:brotli-error |
zstd:zstd-error |
Benchmarks
A simple benchmark test was added to the test system; load it and call run-benchmarks:
(ql:quickload :cl-brotli-tests)
(brotli-benchmarks:run-benchmarks)
Three workloads are measured across 256 B / 4 KiB / 64 KiB / 1 MB payloads of "realistic" SSE-style
text (event: / data: lines with varied counters). The following is captured:
round-trip: one-shot batch API,compress-buffer+decompress-bufferper iteration.stream-flush: streaming (like SSE), one long-lived encoder, onefinish-outputper event. This is the workload that matters for real-time SSE use.decomp-stream: streaming decode, a freshmake-decompressing-streamper iteration.
Reading the output
- Wall(s): elapsed real time ("wall clock time") for all iterations combined.
- MiB/s: input throughput (uncompressed bytes / wall time).
- Ratio: compressed bytes / input bytes; below 1.0 means compression is working.
Ratios improve with payload size because brotli's back-reference window covers more data.
decomp-streamis always--since decompression has no quality setting.
Quality 4 (the default) is chosen for streaming; brotli's upstream default of 11 is designed for static asset pre-compression and is too slow per-event.
Use :level (0–11) and :iterations to explore the speed/ratio trade-off:
(brotli-benchmarks:run-benchmarks :level 0) ; fastest encoder, lower ratio
(brotli-benchmarks:run-benchmarks :level 9) ; slower encoder, better ratio
For a flame-style sb-sprof profile, use the make flame target.
Documentation
This package uses declt for documentation, and there's a "docs" target in the Makefile that
creates the Texinfo file and converts it to html: makeinfo is a requirement for this.
License
GNU LGPL v3 or later
Additional info
Author: Frederico Muñoz / ΛↃ lambda combine