Refreshing Third Party Tokens before they expire using GenServers in Elixir

By Raj Rajhans -
February 4th, 2023
6 minute read

The Problem

When your application depends on third-party APIs that require auth tokens, you encounter the problem of token expiry. Tokens usually have a limited lifespan, and you need to refresh them before they expire. Using expired tokens will result in errors. Getting new token at the time of request will work, but it will increase response time, and what if there’s an error while you’re getting the token? Best solution will be to have something that will be responsible for refreshing the token before it expires. Luckily, we have GenServers for this!

The Solution

I came across this while going through ExAws code, which is a library for interacting with AWS services in Elixir. You can check their implementation here

We will create a GenServer that will cache the token and will be responsible for refreshing it before it expires. We’ll store the token, along with the time of issue and expiry in an ETS table.

How it will work

Our GenServer will expose a get/0 function to get the token. When it’s called for the first time, it will get a token from API, store the date in ETS table, and return it. It will also use Process.send_after/3 to send a message to itself to refresh the token before expiry. When the message is received, it will get a new token from API and store it in ETS table. This way, we will always have a valid token in the cache. As an added check, we’ll also check if the token is expired or not before returning it in the get/0 function. We’ll store the ets table reference in the state of the GenServer.

def get() do
:ets.lookup(__MODULE__, @token_key)
|> refresh_auth_if_required()
|> parse_cached_auth

Refresh leading time

To be on the safe side, we’ll have a refresh_leading_time which will be the time difference between when the token will expire and when we’ll refresh it. For example, say current time is 10:00 AM, and we got a token which will expire in 30 minutes (at 10:30 AM). If refresh_leading_time is 5 minutes, we’ll refresh the token at 10:25 AM. This will give us some buffer time in case the token takes some time to refresh or there are any errors, and we have to retry.

We’ll have a refresh_auth_now/1 function that will take in a reference to the ETS table and will refresh the token. It will also schedule a message to itself to refresh the token before expiry. If there’s an error while getting the token, it will schedule a retry in 5 seconds.

defp refresh_auth_now(ets) do
with {:ok, auth} <- get_auth() do
:ets.insert(ets, {@token_key, auth})
Process.send_after(__MODULE__, :refresh_auth, next_refresh_in(auth))
{:error, _e} ->
# schedule a retry in 5 seconds on error
Process.send_after(__MODULE__, :refresh_auth, 5_000)

next_refresh_in/1 will calculate the time difference between when the token will expire and when we’ll refresh it. It will also subtract the refresh_leading_time from it.

defp next_refresh_in(%{expiration_s: expiration_s, token_issued_at: token_issued_at}) do
expires_in_ms = expiration_s * 1000
token_age_ms = get_token_age_s(token_issued_at) * 1000
next_refresh_in_ms = expires_in_ms - token_age_ms
max(0, next_refresh_in_ms - @refresh_lead_time_ms)
defp get_token_age_s(token_issued_at) do
DateTime.diff(DateTime.utc_now(), token_issued_at, :second)

We will store the actual token, the time at which it was issued, and its expiry in the ETS table. For this, we’ll have the get_auth and parse_auth functions.

defp get_auth() do
with {:ok, token, expiration} <- RandomApi.get_access_token() do
expiration_s: expiration,
token: token,
token_issued_at: DateTime.utc_now()
{:error, e} ->
Logger.error("Error getting the auth token: #{inspect(e)}")
{:error, e}
defp parse_cached_auth(%{token: token}) do
{:ok, token}

Finally, we just need to add genserver callbacks, and some extra functions to maintain the ETS table state in the GenServer’s state. You can check the full code here.

That’s all for this post. I hope you found it useful!



Raj Rajhans

Product Engineer @ invideo