This library contains Common Lisp bindings to the Mongoose web server library written in C. Internally Mongoose is event based and so can handle decent concurrent load, but it is otherwise single-threaded in nature.
This library has no Common Lisp dependencies, and by default Mongoose itself
only requires openssl.
The FFI bindings are hand-written to ensure minimal overhead and dependency load. Currently only SBCL is supported.
| Compiler | Compiles? |
|---|---|
| SBCL | ✅ |
| ECL | ❌ |
| Clasp | ❌ |
| ABCL | ❌ |
| CCL | ❌ |
| Clisp | ❌ |
| Allegro | ❌ |
| LispWorks | ❌ |
This library requires Mongoose to be available on your system.
Mongoose is available from the AUR.
aura -A mongoose
Mongoose is available with Homebrew.
brew install mongoose
By design Mongoose is simple and has no complex build process. Simply acquire
its source code, compile a .so, and make it available as
/usr/lib/libmongoose.so.
git clone https://github.com/cesanta/mongoose.git --depth=1 cd mongoose gcc -shared -fPIC -o libmongoose.so mongoose.c
and then something like:
cd /usr/lib sudo ln -s /path/to/libmongoose.so .
See also the official Mongoose Documentation and the examples directory.
As a rule, the functions and types provided here have the same names as their
underlying C ones, except that the mg_ prefix has been stripped and underscores
are now dashes (e.g. mg_http_serve_dir -> http-serve-dir).
(in-package :mongoose) has been used for brevity in the examples below, but it’s
assumed that you’ll use a package nickname in your own code, perhaps mg.
Note also that for simplicity, no higher level Lisp API has been provided on top of Mongoose itself. We will occasionally be using FFI primitives directly, but you will see that this is not a stressful activity. Some small helper functions have been provided where appropriate.
To run the examples, first:
(asdf:load-system :mongoose)
(asdf:load-system :html) ; For examples that need it.Then open the example you want to run, compile/load just that file, and execute its “main” through the REPL.
I could have provided some macro-based defhandler, etc., but I realized that
such an interface already exists: the FFI itself, which we will embrace in the
spirit of minimalism.
Mongoose operates in a single-threaded “polling loop”.
(loop (mgr-poll mgr 1000))Where mgr is an underlying mg_mgr you’ve already initialized, the 1000
milliseconds is a timeout for accepting incoming connections, and mgr-poll
itself is just a direct call to mg_mgr_poll.
Your “main” will look something like this:
(let ((mgr (make-alien mgr)))
(mgr-init mgr)
(let ((handler (alien-sap (alien-callable-function 'ev-handler))))
(http-listen mgr "http://localhost:8000" handler nil))
(loop (mgr-poll mgr 1000))
(mgr-free mgr)
(free-alien mgr))Of note:
make-alien: this initializes some memory that we immediately populate withmgr-init.(alien-sap (alien-callable-function ...)): Mongoose needs a C-level function pointer to its main event handler, and this is how we produce one from the Lisp side.
But what is 'ev-handler referring to? And how can we pass a pointer to a C
function when we aren’t writing C? The answer is define-alien-callable.
(define-alien-callable ev-handler void ((c (* connection)) (ev int) (ev-data (* t)))
"Handle HTTP events."
(when (= ev +ev-http-msg+)
(let ((hm (cast ev-data (* http-message))))
;; Do whatever!
(foo hm))))As you’ll later see in the Examples, our handler will always take this basic
shape. define-alien-callable enables us to write whatever Lisp we want for our
handler logic, and have C call into it. It even supports hot reloading as usual;
our server can already be running, we can hot recompile just this handler, and
the changes will be immediately reflected.
You can see that the input and output types of the handler are C types. The
(* connection) syntax (etc.) is provided by sb-alien, the main FFI module within
SBCL. You can also see us doing a Pointer Cast via cast to get access to the
request payload - this is standard practice with Mongoose.
If you need access to the fields of C structs, use slot as can be seen in this
example.
We get this for free via http-serve-dir.
See examples/00-static-files.lisp.
Keeping it simple, routing is a matter of extracting the uri field of a
http-message and doing string matching on it.
See examples/03-query-params.lisp.
You’re free to use any HTML library you wish, provided it eventually yields a concrete string to pass through the FFI. For a solution with zero transitive dependencies, consider html.
It is a general requirement of HTTP that headers be postfixed by \r\n. For
single headers you can do this yourself like so:
(defparameter *content-type-text* (format nil "Content-Type: text/plain~c~c" #\return #\linefeed))And for multiple via the headers helper:
(in-package :mongoose)
(headers '("Content-Type: text/plain" "User-Agent: foo"))Content-Type: text/plain User-Agent: foo
Either way, you’re encouraged to preallocate common headers via a defparameter
as shown above.
If no headers are necessary, Mongoose also accepts nil (NULL in C).
(http-reply c 404 nil "")⚠ Coming soon! ⚠
Not yet possible until the recent struct-by-value work in SBCL is released. Mongoose’s TLS functions have one specific place, related to their internal string type, where this is necessary.
Mongoose does its own logging, which you will see if running an actual Lisp
program (but not through the REPL). By default many messages are printed -
enough to lower latency. To change this, simply alter the value of *log-level*.
(in-package :mongoose)
(let ((mgr (make-alien mgr)))
(setf *log-level* +ll-error+)
(mgr-init mgr)
;; ... etc. ...
(mgr-free mgr)
(free-alien mgr))The available log levels are:
+ll-none++ll-error++ll-info++ll-debug+(the default)+ll-verbose+
See sample.c for a trivial baseline server and repl.lisp for its Common Lisp
analogue. The following command:
echo "GET http://localhost:8000/html" | vegeta attack -duration=10s -rate=0 -max-workers=4 | tee results.bin | vegeta report
produces these results on my 2018 ThinkPad (I write this in January 2026). The only attempt to optimize either code was to disable logging.
| Language | Requests-per-Second |
|---|---|
| Lisp | 23600 |
| C | 25000 |
There are some string-related inefficiencies in the Lisp that could easily be worked around in production code, thus I am willing to claim that for practical purposes the FFI can be trusted not to introduce undue overhead.
- HTTPS bindings (coming soon to SBCL!)
- HTTP Client functionality.
- WebSockets server functionality.
Anything else is essentially unplanned but will be considered if there is sufficient demand.