diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
-## [_Unreleased_](https://github.com/freckle/github-app-token/compare/v0.0.1.1...main)
+## [_Unreleased_](https://github.com/freckle/github-app-token/compare/v0.0.1.2...main)
 
-## [v0.0.1.0](https://github.com/freckle/github-app-token/tree/v0.0.1.1)
+## [v0.0.1.2](https://github.com/freckle/github-app-token/compare/v0.0.1.1...v0.0.1.2)
+
+- Implement refresh wake-ups more efficiently
+
+## [v0.0.1.1](https://github.com/freckle/github-app-token/compare/v0.0.0.1...v0.0.1.1)
 
 - Add `GitHub.App.Token.Refresh`
 
diff --git a/github-app-token.cabal b/github-app-token.cabal
--- a/github-app-token.cabal
+++ b/github-app-token.cabal
@@ -1,6 +1,6 @@
 cabal-version:   1.18
 name:            github-app-token
-version:         0.0.1.1
+version:         0.0.1.2
 license:         MIT
 license-file:    LICENSE
 maintainer:      Freckle Education
diff --git a/src/GitHub/App/Token/Refresh.hs b/src/GitHub/App/Token/Refresh.hs
--- a/src/GitHub/App/Token/Refresh.hs
+++ b/src/GitHub/App/Token/Refresh.hs
@@ -8,8 +8,7 @@
 
 import GitHub.App.Token.Prelude
 
-import Control.Monad (forever)
-import Data.Time (getCurrentTime)
+import Data.Time (diffUTCTime, getCurrentTime)
 import Data.Void (Void)
 import GitHub.App.Token.Generate (AccessToken (..))
 import UnliftIO (MonadUnliftIO)
@@ -56,12 +55,25 @@
 refreshing f = do
   x <- f
   ref <- newIORef x
-  thread <- async $ forever $ do
-    threadDelay $ round @Double $ 0.5 * 1000000 -- 0.5s
-    now <- liftIO getCurrentTime
-    isExpired <- (<= now) . expiresAt <$> readIORef ref
-    when isExpired $ writeIORef ref =<< f
+  thread <- async $ loop ref x
   pure Refresh {ref, thread}
+ where
+  loop ref current = do
+    now <- liftIO getCurrentTime
+    threadDelay $ refreshInMicroseconds current now
+
+    updated <- f
+    writeIORef ref updated
+    loop ref updated
+
+refreshInMicroseconds :: HasExpiresAt a => a -> UTCTime -> Int
+refreshInMicroseconds a = do
+  round @Double @Int
+    . (* 0.95) -- refresh a little early
+    . (* 1000000) -- convert to microseconds
+    . realToFrac
+    . max 0 -- negative expiry means refresh right away
+    . diffUTCTime (expiresAt a)
 
 getRefresh :: MonadIO m => Refresh a -> m a
 getRefresh = readIORef . (.ref)
