High-performance HTTP/1.1, HTTP/2, HTTP/3 server for Erlang/OTP 27+
Erlang
56
264 commits
updated Sep 20, 2026
One handler set. Every version of HTTP.
Livery is a BEAM-native web framework that serves the same router and middleware over HTTP/1.1, HTTP/2, and HTTP/3 from a single runtime. WebSocket, WebTransport, Server-Sent Events, OpenAPI, MCP, A2A, and OpenTelemetry-style observability are built-in modules. It is written in the spirit of Axum + Tower + Hyper, on Erlang/OTP.
%% rebar.config
{deps, [{livery, {git, "https://github.com/benoitc/livery.git", {branch, "main"}}}]}.
A handler takes a request value and returns a response value. Compile a router, start a service, and you are serving:
Router = livery_router:compile([
{<<"GET">>, <<"/">>, fun(_Req) -> livery_resp:text(200, <<"hello">>) end}
]),
{ok, _Pid} = livery:start_service(#{http => #{port => 8080}, router => Router}).
$ curl localhost:8080/
hello
Run it now: rebar3 shell, paste the two expressions, then curl.
show_user(Req) ->
Id = livery_req:binding(<<"id">>, Req),
livery_resp:json(200, json:encode(#{id => Id, name => <<"Ada">>})).
Router = livery_router:compile([
{<<"GET">>, <<"/users/:id">>, fun show_user/1},
{<<"POST">>, <<"/users">>, {users, create}} %% {Module, Function}
]).
Middleware is a continuation over immutable values: call(Req, Next, State). Attach it service-wide or per route.
livery:start_service(#{
http => #{port => 8080},
router => Router,
middleware => [
{livery_request_id, undefined},
{livery_access_log, #{}},
{livery_body_limit, #{max => 1048576}}
]
}).
The same router and middleware over H1, H2, and H3, advertising H3 via
Alt-Svc:
livery:start_service(#{
http => #{port => 80},
https => #{port => 443, cert => Cert, key => Key},
http3 => #{port => 443, cert => Cert, key => Key},
router => Router,
alt_svc => advertise
}).
Add alpn => [h2, http1] to the https map and that one TLS port
serves HTTP/2 and HTTP/1.1, chosen per connection:
https => #{port => 443, cert => Cert, key => Key, alpn => [h2, http1]}
Need just one protocol? livery:start_listener(livery_h1, #{port => 8080, router => Router}).
The client mirrors the middleware model outbound: stack timeouts, retries, a circuit breaker, or load balancing around a request.
Client = livery_client:new(#{
base_url => <<"https://api.example.com">>,
stack => [livery_client:timeout(5000), livery_client:retry(#{max => 3})]
}),
{ok, Resp} = livery_client:get(Client, <<"/health">>),
200 = livery_client:status(Resp).
events(_Req) ->
livery_resp:sse(200, fun(Emit) ->
Emit(#{event => <<"tick">>, data => <<"1">>}),
Emit(#{event => <<"tick">>, data => <<"2">>})
end).
Chunked bodies, NDJSON, file responses with byte ranges, WebSocket and WebTransport over H2/H3 work the same way.
Sometimes a handler answers before it has read the request body, like
rejecting an oversized upload with a 413. On HTTP/1.1 the connection
would normally close right after the response, and with a large upload
still in flight the close can reach the client before it reads your
413, so the client sees a reset instead.
Livery handles this for you. When you return a full response before the body is drained, it reads and discards the rest of the inbound body before closing, so the client gets the response:
reject(_Req) ->
livery_resp:json(413, [], <<"{\"error\":\"too_big\"}">>).
The drain is bounded by a budget you set per listener (defaults to no byte cap and a 30 s deadline):
{ok, _} = livery:start_listener(livery_h1, #{
port => 8080,
early_response_drain => {16#400000, 5000}, %% 4 MiB / 5 s
handler => fun reject/1
}).
Override it for a single response, or disable the drain with none:
livery_resp:json(413, [], Body, #{early_response_drain => {16#400000, 5000}}).
The per-response override applies to full responses. Streaming responses (SSE, NDJSON, chunked, files) use the listener budget.
call(Req, Next, State)
pipelines, composable per service or per route, in both directions
(server-inbound and the outbound livery_client).Companion libraries built on Livery, each in its own repo:
Full guides, tutorials, and the generated API reference live at
https://benoitc.github.io/livery/. The same content is in the repo
under docs/: start with the
Quickstart, then the
tutorials and
how-to guides. For contributors, see
AGENTS.md.
Livery is developed and maintained by Enki Multimedia. If your company relies on it, reach out for sponsored support, or sponsor its maintenance via GitHub Sponsors.
Apache-2.0.
Erlang
97.9%
High-performance HTTP/1.1, HTTP/2, HTTP/3 server for Erlang/OTP 27+
Erlang
56
264 commits
updated Sep 20, 2026
One handler set. Every version of HTTP.
Livery is a BEAM-native web framework that serves the same router and middleware over HTTP/1.1, HTTP/2, and HTTP/3 from a single runtime. WebSocket, WebTransport, Server-Sent Events, OpenAPI, MCP, A2A, and OpenTelemetry-style observability are built-in modules. It is written in the spirit of Axum + Tower + Hyper, on Erlang/OTP.
%% rebar.config
{deps, [{livery, {git, "https://github.com/benoitc/livery.git", {branch, "main"}}}]}.
A handler takes a request value and returns a response value. Compile a router, start a service, and you are serving:
Router = livery_router:compile([
{<<"GET">>, <<"/">>, fun(_Req) -> livery_resp:text(200, <<"hello">>) end}
]),
{ok, _Pid} = livery:start_service(#{http => #{port => 8080}, router => Router}).
$ curl localhost:8080/
hello
Run it now: rebar3 shell, paste the two expressions, then curl.
show_user(Req) ->
Id = livery_req:binding(<<"id">>, Req),
livery_resp:json(200, json:encode(#{id => Id, name => <<"Ada">>})).
Router = livery_router:compile([
{<<"GET">>, <<"/users/:id">>, fun show_user/1},
{<<"POST">>, <<"/users">>, {users, create}} %% {Module, Function}
]).
Middleware is a continuation over immutable values: call(Req, Next, State). Attach it service-wide or per route.
livery:start_service(#{
http => #{port => 8080},
router => Router,
middleware => [
{livery_request_id, undefined},
{livery_access_log, #{}},
{livery_body_limit, #{max => 1048576}}
]
}).
The same router and middleware over H1, H2, and H3, advertising H3 via
Alt-Svc:
livery:start_service(#{
http => #{port => 80},
https => #{port => 443, cert => Cert, key => Key},
http3 => #{port => 443, cert => Cert, key => Key},
router => Router,
alt_svc => advertise
}).
Add alpn => [h2, http1] to the https map and that one TLS port
serves HTTP/2 and HTTP/1.1, chosen per connection:
https => #{port => 443, cert => Cert, key => Key, alpn => [h2, http1]}
Need just one protocol? livery:start_listener(livery_h1, #{port => 8080, router => Router}).
The client mirrors the middleware model outbound: stack timeouts, retries, a circuit breaker, or load balancing around a request.
Client = livery_client:new(#{
base_url => <<"https://api.example.com">>,
stack => [livery_client:timeout(5000), livery_client:retry(#{max => 3})]
}),
{ok, Resp} = livery_client:get(Client, <<"/health">>),
200 = livery_client:status(Resp).
events(_Req) ->
livery_resp:sse(200, fun(Emit) ->
Emit(#{event => <<"tick">>, data => <<"1">>}),
Emit(#{event => <<"tick">>, data => <<"2">>})
end).
Chunked bodies, NDJSON, file responses with byte ranges, WebSocket and WebTransport over H2/H3 work the same way.
Sometimes a handler answers before it has read the request body, like
rejecting an oversized upload with a 413. On HTTP/1.1 the connection
would normally close right after the response, and with a large upload
still in flight the close can reach the client before it reads your
413, so the client sees a reset instead.
Livery handles this for you. When you return a full response before the body is drained, it reads and discards the rest of the inbound body before closing, so the client gets the response:
reject(_Req) ->
livery_resp:json(413, [], <<"{\"error\":\"too_big\"}">>).
The drain is bounded by a budget you set per listener (defaults to no byte cap and a 30 s deadline):
{ok, _} = livery:start_listener(livery_h1, #{
port => 8080,
early_response_drain => {16#400000, 5000}, %% 4 MiB / 5 s
handler => fun reject/1
}).
Override it for a single response, or disable the drain with none:
livery_resp:json(413, [], Body, #{early_response_drain => {16#400000, 5000}}).
The per-response override applies to full responses. Streaming responses (SSE, NDJSON, chunked, files) use the listener budget.
call(Req, Next, State)
pipelines, composable per service or per route, in both directions
(server-inbound and the outbound livery_client).Companion libraries built on Livery, each in its own repo:
Full guides, tutorials, and the generated API reference live at
https://benoitc.github.io/livery/. The same content is in the repo
under docs/: start with the
Quickstart, then the
tutorials and
how-to guides. For contributors, see
AGENTS.md.
Livery is developed and maintained by Enki Multimedia. If your company relies on it, reach out for sponsored support, or sponsor its maintenance via GitHub Sponsors.
Apache-2.0.
Erlang
97.9%