sturdy-bulkhead implements middleware to queue compute-intensive requests within a Ring API.
It prevents CPU starvation by offloading work to dedicated worker pools using core.async.
Add to deps.edn:
{:deps com.sturdystats/sturdy-bulkhead {:mvn/version "VERSION"}}
Initialize a worker pool when your application starts.
(require '[sturdy.bulkhead :as bulkhead])
(def my-pool
(bulkhead/start-pool! {:num-workers 4 ; 4 concurrent queries max
:queue-size 20})) ; queue up to 20 requests
Use the provided middleware to wrap computationally expensive routes.
(def app
(-> heavy-query-handler
(bulkhead/wrap-compute-bound my-pool {:timeout-ms 10000})))
If you use reitit, you can attach the middleware to specific routes using its vector syntax:
(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
["/api"
["/heavy" {:get {:middleware [[bulkhead/wrap-compute-bound my-pool {:timeout-ms 10000}]]
:handler heavy-query-handler}}]
["/light" {:get {:handler light-query-handler}}]])))
Call stop-pool! as part of application shutdown, after the server has stopped accepting new traffic:
(bulkhead/stop-pool! my-pool)
Stopping a pool closes it permanently; a stopped pool is not intended to be restarted or reused.
Queued and subsequent requests receive the same 503 Service Unavailable response as requests rejected because the queue is full, and all of these cases increment the :rejections statistic.
The pool deliberately does not track separate lifecycle state or distinguish shutdown from saturation.
If that distinction matters for operational metrics, stop accepting traffic before calling stop-pool! and exclude the shutdown window from saturation alerts.
stop-pool! returns after closing the pool and releasing queued callers; it does not wait for active handlers to finish.
Active handlers continue running until they finish and may keep their worker threads alive after stop-pool! returns.
stop-pool! does not interrupt or cancel handlers because arbitrary synchronous handler code cannot be safely stopped by the pool.
Applications that require stronger shutdown guarantees must use cooperative cancellation in their handlers or manage cancellation at a higher level.
The pool's :processed statistic counts every handler that completes, including handlers that throw.
The :errors statistic counts handler attempts that throw, including failures that occur after the caller has timed out.
Use (bulkhead/pool-stats my-pool) to read a snapshot of the pool's current statistics without depending on its internal representation.
It is critical to tune the worker pool relative to your web server's thread pool:
The sum of your num-workers and queue-size should be strictly less than your web server's maximum thread pool size.
Because wrap-compute-bound is synchronous middleware, it blocks the incoming Jetty request thread while waiting for the bulkhead worker to finish (or for the timeout to occur).
num-workers): This should be less than your total CPU cores. This ensures the OS and web server always have dedicated resources to process network I/O, health checks, and fast endpoints without CPU starvation.queue-size): Determine how many requests you are willing to hold in memory during a traffic burst. If the queue is full, the middleware immediately returns a 503 Service Unavailable, freeing the jetty thread. Note that you may configure :queue-size 0, which means requests will only be accepted if a worker is immediately available for direct handoff; otherwise they are rejected.num-workers = 30 and queue-size = 250, all of jetty's threads could be sleeping on responses from bulkhead. This would render the server unresponsive until the queue drains.:timeout-ms), the caller immediately receives a 504 Gateway Timeout.
However, if the worker pool has already begun executing your handler, that handler will continue running to completion in the background, occupying a worker thread until it finishes.
Timeouts only free the incoming web server thread; they do not interrupt or cancel the underlying worker pool task.
A timed-out handler may therefore outlive server shutdown.This project uses deps.edn aliases for testing and validation.
# Run unit tests
clj -X:test
# Generate code coverage report (in target/coverage/index.html)
clj -X:coverage
# Scan dependencies for known vulnerabilities (clj-watson)
clj -X:vuln
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |