diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,8 @@
-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.15.1.0...main)
+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.15.2.0...main)
+
+## [v1.15.0.1](https://github.com/freckle/freckle-app/compare/v1.15.2.0...v1.15.1.0)
+
+- Add `FaktoryProducerPool` and `HasFaktoryProducerPool` for pooling connections to Faktory
 
 ## [v1.15.1.0](https://github.com/freckle/freckle-app/compare/v1.15.0.1...v1.15.1.0)
 
diff --git a/freckle-app.cabal b/freckle-app.cabal
--- a/freckle-app.cabal
+++ b/freckle-app.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.18
 name:               freckle-app
-version:            1.15.1.0
+version:            1.15.2.0
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
@@ -37,6 +37,7 @@
         Freckle.App.Exception.MonadThrow
         Freckle.App.Exception.MonadUnliftIO
         Freckle.App.Exception.Types
+        Freckle.App.Faktory.ProducerPool
         Freckle.App.Ghci
         Freckle.App.GlobalCache
         Freckle.App.Http
@@ -128,6 +129,7 @@
         errors >=2.3.0,
         exceptions >=0.10.4,
         extra >=1.7.13,
+        faktory >=1.1.2.4,
         filepath >=1.4.2.2,
         hashable >=1.4.2.0,
         hs-opentelemetry-api >=0.1.0.0,
diff --git a/library/Freckle/App/Faktory/ProducerPool.hs b/library/Freckle/App/Faktory/ProducerPool.hs
new file mode 100644
--- /dev/null
+++ b/library/Freckle/App/Faktory/ProducerPool.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE ApplicativeDo #-}
+
+module Freckle.App.Faktory.ProducerPool
+  ( FaktoryProducerPool
+  , FaktoryProducerPoolConfig (..)
+  , envFaktoryProducerPoolConfig
+  , HasFaktoryProducerPool (..)
+  , createFaktoryProducerPool
+  ) where
+
+import Freckle.App.Prelude
+
+import Control.Lens (Lens')
+import Data.Pool
+  ( Pool
+  , defaultPoolConfig
+  , newPool
+  , setNumStripes
+  )
+import qualified Faktory.Producer as Faktory
+import qualified Faktory.Settings as Faktory
+import qualified Freckle.App.Env as Env
+import Yesod.Core.Lens (envL, siteL)
+import Yesod.Core.Types (HandlerData)
+
+data FaktoryProducerPoolConfig = FaktoryProducerPoolConfig
+  { faktoryProducerPoolConfigStripes :: Int
+  -- ^ The number of stripes (distinct sub-pools) to maintain.
+  -- The smallest acceptable value is 1.
+  , faktoryProducerPoolConfigIdleTimeout :: NominalDiffTime
+  -- ^ Amount of time for which an unused resource is kept open.
+  -- The smallest acceptable value is 0.5 seconds.
+  --
+  -- The elapsed time before destroying a resource may be a little
+  -- longer than requested, as the reaper thread wakes at 1-second
+  -- intervals.
+  , faktoryProducerPoolConfigSize :: Int
+  -- ^ Maximum number of resources to keep open per stripe.  The
+  -- smallest acceptable value is 1.
+  --
+  -- Requests for resources will block if this limit is reached on a
+  -- single stripe, even if other stripes have idle resources
+  -- available.
+  }
+  deriving stock (Show)
+
+-- | Same defaults as 'Database.Persist.Sql.ConnectionPoolConfig'
+defaultFaktoryProducerPoolConfig :: FaktoryProducerPoolConfig
+defaultFaktoryProducerPoolConfig = FaktoryProducerPoolConfig 1 600 10
+
+envFaktoryProducerPoolConfig
+  :: Env.Parser Env.Error FaktoryProducerPoolConfig
+envFaktoryProducerPoolConfig = do
+  poolSize <- Env.var Env.auto "FAKTORY_PRODUCER_POOL_SIZE" $ Env.def 10
+  pure $
+    defaultFaktoryProducerPoolConfig {faktoryProducerPoolConfigSize = poolSize}
+
+type FaktoryProducerPool = Pool Faktory.Producer
+
+class HasFaktoryProducerPool env where
+  faktoryProducerPoolL :: Lens' env FaktoryProducerPool
+
+instance HasFaktoryProducerPool site => HasFaktoryProducerPool (HandlerData child site) where
+  faktoryProducerPoolL = envL . siteL . faktoryProducerPoolL
+
+createFaktoryProducerPool
+  :: Faktory.Settings -> FaktoryProducerPoolConfig -> IO FaktoryProducerPool
+createFaktoryProducerPool faktorySettings poolConfig =
+  newPool
+    $ setNumStripes
+      (Just $ faktoryProducerPoolConfigStripes poolConfig)
+    $ defaultPoolConfig
+      (Faktory.newProducer faktorySettings)
+      Faktory.closeProducer
+      (realToFrac $ faktoryProducerPoolConfigIdleTimeout poolConfig)
+      (faktoryProducerPoolConfigSize poolConfig)
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
 name: freckle-app
-version: 1.15.1.0
+version: 1.15.2.0
 maintainer: Freckle Education
 category: Utils
 github: freckle/freckle-app
@@ -96,6 +96,7 @@
     - errors
     - exceptions
     - extra
+    - faktory
     - filepath
     - hashable
     - hs-opentelemetry-api
