Configuration Reference

This document covers all Hornbeam configuration options.

Quick Start

hornbeam:start("myapp:application", #{
    bind => "0.0.0.0:8000",
    workers => 4,
    worker_class => asgi
}).

Server Options

OptionTypeDefaultDescription
bindbinary/string"127.0.0.1:8000"Address and port to bind to
sslbooleanfalseEnable SSL/TLS
certfilebinaryundefinedPath to SSL certificate
keyfilebinaryundefinedPath to SSL private key
cacertfilebinaryundefinedPath to CA certificate

SSL Example

hornbeam:start("app:application", #{
    bind => "0.0.0.0:443",
    ssl => true,
    certfile => "/path/to/cert.pem",
    keyfile => "/path/to/key.pem"
}).

Protocol Options

OptionTypeDefaultDescription
worker_classatomwsgiProtocol: wsgi or asgi
http_versionlist['HTTP/1.1', 'HTTP/2']Supported HTTP versions

Worker Options

OptionTypeDefaultDescription
workersinteger4Number of Python workers
timeoutinteger30000Request timeout in milliseconds
keepaliveinteger2HTTP keep-alive timeout in seconds
max_requestsinteger1000Recycle worker after N requests
preload_appbooleanfalseLoad app before forking workers

Worker Sizing

%% CPU-bound (ML inference)
hornbeam:start("app:app", #{
    workers => erlang:system_info(schedulers)  % One per CPU
}).

%% I/O-bound (web app with DB calls)
hornbeam:start("app:app", #{
    workers => erlang:system_info(schedulers) * 2
}).

ASGI Options

OptionTypeDefaultDescription
lifespanatomautoLifespan handling: auto, on, off
root_pathbinary""ASGI root_path for mounted apps

Lifespan Values

  • auto - Detect if app supports lifespan, use if available
  • on - Require lifespan, fail if app doesn’t support it
  • off - Disable lifespan even if app supports it

WebSocket Options

OptionTypeDefaultDescription
websocket_timeoutinteger60000WebSocket idle timeout (ms)
websocket_max_frame_sizeinteger16777216Max frame size (16MB)
websocket_compressbooleanfalseEnable WebSocket compression

Python Options

OptionTypeDefaultDescription
pythonpathlist[".", "examples"]Python module search paths
python_homebinaryundefinedPython home directory
venvbinaryundefinedVirtual environment path

Virtual Environment

hornbeam:start("app:application", #{
    venv => "/path/to/myproject/venv",
    pythonpath => ["/path/to/myproject"]
}).

Hooks

Hooks allow you to execute Erlang code at key points in the request lifecycle.

HookArgumentsReturnDescription
on_requestRequestRequestCalled before handling request
on_responseResponseResponseCalled before sending response
on_errorError, Request{Code, Body}Called on error
on_worker_startWorkerIdokCalled when worker starts
on_worker_exitWorkerId, ReasonokCalled when worker exits

Hook Examples

hornbeam:start("app:application", #{
    hooks => #{
        %% Request hook: logging, authentication, rate limiting
        on_request => fun(Request) ->
            #{method := Method, path := Path} = Request,
            logger:info("~s ~s", [Method, Path]),

            %% Add custom header
            Headers = maps:get(headers, Request, #{}),
            Request#{headers => Headers#{<<"x-request-id">> => generate_id()}}
        end,

        %% Response hook: add headers, log response
        on_response => fun(Response) ->
            #{status := Status} = Response,
            metrics:incr(<<"response_", (integer_to_binary(Status))/binary>>),

            %% Add server header
            Headers = maps:get(headers, Response, #{}),
            Response#{headers => Headers#{<<"x-powered-by">> => <<"Hornbeam">>}}
        end,

        %% Error hook: custom error handling
        on_error => fun(Error, Request) ->
            #{path := Path} = Request,
            logger:error("Error on ~s: ~p", [Path, Error]),

            case Error of
                {timeout, _} ->
                    {504, <<"Gateway Timeout">>};
                {python_error, Reason} ->
                    {500, iolist_to_binary(io_lib:format("~p", [Reason]))};
                _ ->
                    {500, <<"Internal Server Error">>}
            end
        end,

        %% Worker lifecycle hooks
        on_worker_start => fun(WorkerId) ->
            logger:info("Worker ~p started", [WorkerId]),
            ok
        end,

        on_worker_exit => fun(WorkerId, Reason) ->
            logger:warning("Worker ~p exited: ~p", [WorkerId, Reason]),
            ok
        end
    }
}).

Authentication Hook

hornbeam:start("app:application", #{
    hooks => #{
        on_request => fun(Request) ->
            #{headers := Headers, path := Path} = Request,

            %% Skip auth for public paths
            case Path of
                <<"/health">> -> Request;
                <<"/public/", _/binary>> -> Request;
                _ ->
                    case maps:get(<<"authorization">>, Headers, undefined) of
                        undefined ->
                            throw({unauthorized, <<"Missing authorization header">>});
                        Token ->
                            case auth:verify_token(Token) of
                                {ok, UserId} ->
                                    Request#{user_id => UserId};
                                {error, _} ->
                                    throw({unauthorized, <<"Invalid token">>})
                            end
                    end
            end
        end,

        on_error => fun(Error, _Request) ->
            case Error of
                {unauthorized, Message} ->
                    {401, Message};
                _ ->
                    {500, <<"Internal Server Error">>}
            end
        end
    }
}).

Rate Limiting Hook

hornbeam:start("app:application", #{
    hooks => #{
        on_request => fun(Request) ->
            #{headers := Headers} = Request,
            ClientIP = maps:get(<<"x-forwarded-for">>, Headers,
                                maps:get(<<"x-real-ip">>, Headers, <<"unknown">>)),

            %% Rate limit key
            Minute = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) div 60,
            Key = {rate_limit, ClientIP, Minute},

            %% Check and increment
            Count = ets:update_counter(rate_limits, Key, 1, {Key, 0}),

            if
                Count > 100 ->
                    throw({rate_limited, <<"Too many requests">>});
                true ->
                    Request
            end
        end,

        on_error => fun(Error, _Request) ->
            case Error of
                {rate_limited, Message} ->
                    {429, Message};
                _ ->
                    {500, <<"Internal Server Error">>}
            end
        end
    }
}).

Metrics Hook

hornbeam:start("app:application", #{
    hooks => #{
        on_request => fun(Request) ->
            %% Start timing
            Request#{start_time => erlang:monotonic_time(microsecond)}
        end,

        on_response => fun(Response) ->
            #{start_time := StartTime, status := Status, path := Path} = Response,
            Duration = erlang:monotonic_time(microsecond) - StartTime,

            %% Record metrics
            prometheus_histogram:observe(
                http_request_duration_microseconds,
                [Path, Status],
                Duration
            ),

            Response
        end
    }
}).

sys.config

Configure via sys.config for releases:

%% config/sys.config
[
    {hornbeam, [
        {bind, "0.0.0.0:8000"},
        {workers, 8},
        {worker_class, asgi},
        {timeout, 30000},
        {lifespan, on},
        {pythonpath, [".", "src"]},
        {websocket_timeout, 120000}
    ]}
].

Then start without options:

hornbeam:start("app:application").

Environment Variables

# Set via environment
export HORNBEAM_BIND="0.0.0.0:8000"
export HORNBEAM_WORKERS=8
export HORNBEAM_TIMEOUT=30000
%% Read from environment
hornbeam:start("app:application", #{
    bind => os:getenv("HORNBEAM_BIND", "127.0.0.1:8000"),
    workers => list_to_integer(os:getenv("HORNBEAM_WORKERS", "4"))
}).

Next Steps