Erlang retry with capped exponential backoff

Erlang
-module(retry_backoff).
-export([with_backoff/2]).

with_backoff(Fun, Opts) ->
    Max = maps:get(max_attempts, Opts, 5),
    Base = maps:get(base_ms, Opts, 100),
    Cap = maps:get(max_ms, Opts, 5000),
    attempt(Fun, 1, Max, Base, Cap).

attempt(_Fun, N, Max, _Base, _Cap) when N > Max ->
    {error, exhausted};
attempt(Fun, N, Max, Base, Cap) ->
    case Fun() of
        {ok, _} = Ok -> Ok;
        {error, _} = Err when N =:= Max -> Err;
        {error, _} ->
            Delay = min(Cap, Base * (1 bsl (N - 1))),
            timer:sleep(Delay + rand:uniform(max(1, Delay div 2))),
            attempt(Fun, N + 1, Max, Base, Cap)
    end.