| name | clojure-ring-jetty-adapter |
| description | Ring Jetty adapter for running ring-compatible web servers in Clojure. Use when you are working with ring and jetty starting and need to know to configure or interact with a ring jetty |
Ring Jetty Adapter
The Ring Jetty adapter runs Ring handlers on an embedded Jetty 12 server. It's Ring-compatible and supports HTTP, HTTPS, WebSockets, and Unix domain sockets.
Setup
deps.edn:
ring/ring-jetty-adapter {:mvn/version "1.15.3"}
Leiningen:
[ring/ring-jetty-adapter "1.15.3"]
See https://clojars.org/ring/ring-jetty-adapter for the latest version.
Quick Start
Basic synchronous handler:
(require '[ring.adapter.jetty :refer [run-jetty]])
(defn handler [request]
{:status 200
:headers {"Content-Type" "text/html"}
:body "Hello World"})
(run-jetty handler {:port 3000})
Non-blocking server for REPL:
;; Set :join? false to return control to REPL
(def server (run-jetty handler {:port 3000 :join? false}))
Server Lifecycle
Starting a Server
run-jetty returns a org.eclipse.jetty.server.Server instance:
(def server (run-jetty handler {:port 3000 :join? false}))
With :join? true (default), the function blocks until the server stops. With :join? false, it returns immediately.
Stopping a Server
Call .stop on the server instance:
(.stop server)
Or use it in a component lifecycle library:
;; Component/Integrant/Mount pattern
(defn start-server [handler config]
(run-jetty handler (merge config {:join? false})))
(defn stop-server [server]
(.stop server))
Graceful Shutdown
The server stops gracefully by default, completing in-flight requests:
(.stop server) ; Waits for current requests to complete
Basic Configuration
Port and Host
(run-jetty handler
{:port 3000 ; Listen port (default 80)
:host "localhost"}) ; Listen host (default all interfaces)
Thread Pool
Control worker threads:
(run-jetty handler
{:min-threads 8 ; Minimum threads (default 8)
:max-threads 50 ; Maximum threads (default 50)
:thread-idle-timeout 60000 ; Thread idle timeout in ms (default 60000)
:max-queued-requests 100}) ; Max queued requests (default Integer/MAX_VALUE)
Use daemon threads (useful for testing):
(run-jetty handler {:daemon? true})
Custom thread pool:
(import '[java.util.concurrent Executors])
(run-jetty handler
{:thread-pool (Executors/newVirtualThreadPerTaskExecutor)}) ; Java 19+
Connection Configuration
(run-jetty handler
{:max-idle-time 200000 ; Connection idle timeout in ms (default 200000)
:acceptor-threads 2 ; Number of acceptor threads (default -1, auto)
:selector-threads 4}) ; Number of selector threads (default -1, auto)
HTTP Configuration
(run-jetty handler
{:output-buffer-size 32768 ; Response buffer size (default 32768)
:request-header-size 8192 ; Max request header size (default 8192)
:response-header-size 8192 ; Max response header size (default 8192)
:send-date-header? true ; Include Date header (default true)
:send-server-version? true}) ; Include Server header (default true)
SSL/HTTPS Configuration
Basic SSL
(run-jetty handler
{:ssl? true ; Enable SSL
:ssl-port 443 ; SSL port (default 443, implies :ssl? true)
:keystore "path/to/keystore.jks"
:key-password "password"})
Advanced SSL Options
(run-jetty handler
{:ssl-port 8443
:keystore "keystore.jks" ; Path to keystore or KeyStore instance
:keystore-type "jks" ; Keystore type (default "jks")
:key-password "secret"
:truststore "truststore.jks" ; Optional truststore
:trust-password "secret"
:client-auth :need}) ; :need, :want, or :none (default :none)
Using an SSLContext directly:
(import '[javax.net.ssl SSLContext])
(run-jetty handler
{:ssl-port 8443
:ssl-context (SSLContext/getInstance "TLS")})
SSL Cipher and Protocol Control
(run-jetty handler
{:ssl? true
:keystore "keystore.jks"
:key-password "secret"
:exclude-ciphers ["SSL_RSA_WITH_DES_CBC_SHA"]
:exclude-protocols ["SSLv3" "TLSv1"]
:replace-exclude-ciphers? false ; false = add to defaults (default)
:replace-exclude-protocols? false ; true = replace defaults
:sni-host-check? true}) ; SNI hostname check (default true)
Keystore Auto-Reload
Automatically reload keystore when it changes:
(run-jetty handler
{:ssl? true
:keystore "keystore.jks"
:key-password "secret"
:keystore-scan-interval 60}) ; Check every 60 seconds
HTTP and HTTPS Together
Run both HTTP and HTTPS:
(run-jetty handler
{:port 8080 ; HTTP port
:ssl-port 8443 ; HTTPS port
:keystore "keystore.jks"
:key-password "secret"})
Disable HTTP (HTTPS only):
(run-jetty handler
{:http? false ; Disable HTTP
:ssl-port 8443
:keystore "keystore.jks"
:key-password "secret"})
WebSockets
Ring 1.11+ supports WebSockets through the ring.websocket namespace.
Basic WebSocket handler:
(require '[ring.websocket :as ws])
(defn ws-handler [request]
(if (ws/upgrade-request? request)
{::ws/listener
{:on-open
(fn [socket]
(ws/send socket "Welcome!"))
:on-message
(fn [socket message]
(ws/send socket (str "Echo: " message)))
:on-close
(fn [socket code reason]
(println "Closed:" code reason))}}
{:status 426
:body "WebSocket upgrade required"}))
(run-jetty ws-handler {:port 3000 :join? false})
WebSocket Listener Events
All available events:
{::ws/listener
{:on-open (fn [socket] ...) ; WebSocket opened
:on-message (fn [socket message] ...) ; Message received (String or ByteBuffer)
:on-ping (fn [socket buffer] ...) ; PING received (optional)
:on-pong (fn [socket buffer] ...) ; PONG received
:on-error (fn [socket throwable] ...) ; Error occurred
:on-close (fn [socket code reason] ...)}} ; WebSocket closed
WebSocket Socket Operations
Send messages:
;; Synchronous send
(ws/send socket "text message")
(ws/send socket byte-array)
(ws/send socket byte-buffer)
;; Asynchronous send with callbacks
(ws/send socket "message"
(fn [] (println "Success"))
(fn [ex] (println "Failed:" ex)))
Ping/pong:
(ws/ping socket) ; Send PING
(ws/ping socket byte-buffer) ; PING with data
(ws/pong socket) ; Send unsolicited PONG
(ws/pong socket byte-buffer) ; PONG with data
Close connection:
(ws/close socket) ; Normal close
(ws/close socket 1000 "Goodbye") ; Close with code and reason
(ws/open? socket) ; Check if open
WebSocket Configuration
Configure WebSocket limits:
(run-jetty ws-handler
{:port 3000
:ws-idle-timeout 30000 ; Idle timeout in ms (default 30000)
:ws-max-text-size 65536 ; Max text message size in bytes (default 65536)
:ws-max-binary-size 65536}) ; Max binary message size in bytes (default 65536)
WebSocket Subprotocols
Specify accepted subprotocol:
(defn ws-handler [request]
(if (ws/upgrade-request? request)
{::ws/listener {...}
::ws/protocol "my-protocol"} ; Accepted subprotocol
{:status 426 :body "WebSocket upgrade required"}))
Async Handlers
For async/streaming handlers, use :async? true:
(defn async-handler [request respond raise]
(future
(try
(respond {:status 200 :body "Async response"})
(catch Exception e
(raise e)))))
(run-jetty async-handler
{:async? true
:async-timeout 30000 ; Timeout in ms (default 0, no timeout)
:async-timeout-handler ; Optional timeout handler
(fn [request respond raise]
(respond {:status 408 :body "Request timeout"}))})
The async handler receives three arguments:
request- the Ring request maprespond- function to call with response mapraise- function to call with exception
Unix Domain Sockets
Listen on a Unix domain socket:
(run-jetty handler
{:unix-socket "/tmp/my-app.sock"})
The socket file is automatically deleted on JVM exit.
Advanced Configuration
Custom Server Configuration
Use :configurator for direct server access:
(run-jetty handler
{:port 3000
:configurator
(fn [^org.eclipse.jetty.server.Server server]
;; Direct Jetty configuration
(.setStopAtShutdown server true)
(.setStopTimeout server 5000))})
Example: Production Configuration
(defn start-production-server [handler]
(run-jetty handler
{:port 8080
:host "0.0.0.0"
:join? false
:min-threads 10
:max-threads 100
:max-queued-requests 1000
:max-idle-time 300000
:send-server-version? false ; Don't leak server version
:configurator
(fn [server]
(.setStopAtShutdown server true)
(.setStopTimeout server 30000))}))
Common Patterns
Component Lifecycle
Using with component libraries:
;; Stuart Sierra's Component
(defrecord WebServer [handler port server]
component/Lifecycle
(start [this]
(if server
this
(assoc this :server
(run-jetty handler {:port port :join? false}))))
(stop [this]
(when server
(.stop server))
(assoc this :server nil)))
;; Integrant
(defmethod ig/init-key :adapter/jetty [_ {:keys [handler options]}]
(run-jetty handler (merge options {:join? false})))
(defmethod ig/halt-key! :adapter/jetty [_ server]
(.stop server))
REPL Development
For interactive development:
(defonce server (atom nil))
(defn start! []
(reset! server
(run-jetty #'app {:port 3000 :join? false})))
(defn stop! []
(when @server
(.stop @server)
(reset! server nil)))
(defn restart! []
(stop!)
(start!))
Using #'app (var instead of function) allows reloading handler without restart.
Production Deployment
For production as an uberjar:
(ns myapp.main
(:require [ring.adapter.jetty :refer [run-jetty]])
(:gen-class))
(defn -main [& args]
(let [port (Integer/parseInt (or (System/getenv "PORT") "8080"))]
(run-jetty handler {:port port})))
In project.clj:
{:profiles
{:uberjar
{:aot :all
:main myapp.main}}}
Build and run:
lein uberjar
PORT=8080 java -jar target/myapp-standalone.jar
Key Gotchas
Join Behavior
By default :join? true blocks forever. Always use :join? false in:
- REPL sessions
- Component lifecycle systems
- Tests
- Any context where you need control flow to continue
Thread Pool Sizing
Default thread pool (8-50 threads) is conservative. For high-concurrency apps:
{:min-threads 50
:max-threads 200
:max-queued-requests 5000}
For async handlers or virtual threads (Java 19+), fewer threads needed.
WebSocket Upgrade Check
Always check ws/upgrade-request? before returning WebSocket response:
(defn handler [request]
(if (ws/upgrade-request? request)
{::ws/listener {...}}
{:status 426 :body "WebSocket upgrade required"}))
SSL Client Authentication
:client-auth options:
:none(default) - no client cert required:want- request client cert, optional:need- require client cert, connection fails without it
Server Version Header
Set :send-server-version? false in production to avoid leaking Jetty version.
Comparison with http-kit
vs http-kit server:
- Jetty: Mature, battle-tested, feature-rich, heavier (~10MB with deps)
- http-kit: Lightweight (~90KB), fast, zero dependencies, simpler API
- Jetty: Better SSL/TLS configurability
- http-kit: Built-in HTTP client (Jetty is server-only)
- Both: Ring-compatible, WebSocket support, production-ready
Choose Jetty for:
- Complex SSL requirements
- Need servlet features
- Already using Jetty infrastructure
Choose http-kit for:
- Minimal dependencies
- High concurrency with low memory
- Need both client and server
- Simpler deployment
References
- Ring Wiki: https://github.com/ring-clojure/ring/wiki
- API Docs: https://cljdoc.org/d/ring/ring-jetty-adapter/
- Jetty Documentation: https://www.eclipse.org/jetty/documentation/