diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## [_Unreleased_](https://github.com/freckle/freckle-app/compare/freckle-app-v1.23.0.1...main)
 
+## [v1.23.1.0](https://github.com/freckle/freckle-app/compare/freckle-app-v1.23.0.1...freckle-app-v1.23.1.0)
+
+- Add `pooledMapConcurrently` et-al to `Freckle.App.Async`
+
 ## [v1.23.0.1](https://github.com/freckle/freckle-app/compare/freckle-app-v1.23.0.0...freckle-app-v1.23.0.1)
 
 Add more thread context preserving combinators to `Freckle.App.Async`
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.22
 name:               freckle-app
-version:            1.23.0.1
+version:            1.23.1.0
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
diff --git a/library/Freckle/App/Async.hs b/library/Freckle/App/Async.hs
--- a/library/Freckle/App/Async.hs
+++ b/library/Freckle/App/Async.hs
@@ -1,15 +1,30 @@
 module Freckle.App.Async
-  ( async
-  , foldConcurrently
-  , immortalCreate
-  , immortalCreateLogged
-  , ThreadContext (..)
+  ( -- * The 'ThreadContext' these functions preserve
+    ThreadContext (..)
   , getThreadContext
   , withThreadContext
-  , forConcurrently
-  , forConcurrently_
+
+    -- * "UnliftIO.Async" functions that preserve context
+  , async
+  , pooledMapConcurrentlyN
+  , pooledMapConcurrently
+  , pooledMapConcurrentlyN_
+  , pooledMapConcurrently_
+  , pooledForConcurrentlyN
+  , pooledForConcurrently
+  , pooledForConcurrentlyN_
+  , pooledForConcurrently_
   , mapConcurrently
+  , forConcurrently
   , mapConcurrently_
+  , forConcurrently_
+
+    -- ** Related extensions
+  , foldConcurrently
+
+    -- * "Control.Immortal"-related functions
+  , immortalCreate
+  , immortalCreateLogged
   ) where
 
 import Freckle.App.Prelude
@@ -34,20 +49,58 @@
   context <- getThreadContext
   UnliftIO.async $ withThreadContext context f
 
--- | Run a list of actions concurrently
---
--- The forked threads will have the current thread context copied to them.
-foldConcurrently
-  :: (MonadUnliftIO m, MonadMask m, Monoid a, Foldable t) => t (m a) -> m a
-foldConcurrently xs = do
+-- | 'UnliftIO.Async.pooledMapConcurrentlyN' but passing the thread context along
+pooledMapConcurrentlyN
+  :: (MonadUnliftIO m, MonadMask m, Traversable t)
+  => Int -> (a -> m b) -> t a -> m (t b)
+pooledMapConcurrentlyN n f xs = do
   context <- getThreadContext
-  runConc $ foldMap (conc . withThreadContext context) xs
+  UnliftIO.pooledMapConcurrentlyN n (withThreadContext context . f) xs
 
--- | 'UnliftIO.Async.forConcurrently' but passing the thread context along
-forConcurrently
+-- | 'UnliftIO.Async.pooledMapConcurrently' but passing the thread context along
+pooledMapConcurrently
+  :: (MonadUnliftIO m, MonadMask m, Traversable t) => (a -> m b) -> t a -> m (t b)
+pooledMapConcurrently f xs = do
+  context <- getThreadContext
+  UnliftIO.pooledMapConcurrently (withThreadContext context . f) xs
+
+-- | 'UnliftIO.Async.pooledMapConcurrentlyN_' but passing the thread context along
+pooledMapConcurrentlyN_
+  :: (MonadUnliftIO m, MonadMask m, Traversable t)
+  => Int -> (a -> m b) -> t a -> m ()
+pooledMapConcurrentlyN_ n f xs = do
+  context <- getThreadContext
+  UnliftIO.pooledMapConcurrentlyN_ n (withThreadContext context . f) xs
+
+-- | 'UnliftIO.Async.pooledMapConcurrently_' but passing the thread context along
+pooledMapConcurrently_
+  :: (MonadUnliftIO m, MonadMask m, Traversable t) => (a -> m b) -> t a -> m ()
+pooledMapConcurrently_ f xs = do
+  context <- getThreadContext
+  UnliftIO.pooledMapConcurrently_ (withThreadContext context . f) xs
+
+-- | 'UnliftIO.Async.pooledForConcurrentlyN' but passing the thread context along
+pooledForConcurrentlyN
+  :: (MonadUnliftIO m, MonadMask m, Traversable t)
+  => Int -> t a -> (a -> m b) -> m (t b)
+pooledForConcurrentlyN n = flip $ pooledMapConcurrentlyN n
+
+-- | 'UnliftIO.Async.pooledForConcurrently' but passing the thread context along
+pooledForConcurrently
   :: (MonadUnliftIO m, MonadMask m, Traversable t) => t a -> (a -> m b) -> m (t b)
-forConcurrently = flip mapConcurrently
+pooledForConcurrently = flip pooledMapConcurrently
 
+-- | 'UnliftIO.Async.pooledForConcurrentlyN_' but passing the thread context along
+pooledForConcurrentlyN_
+  :: (MonadUnliftIO m, MonadMask m, Traversable t)
+  => Int -> t a -> (a -> m b) -> m ()
+pooledForConcurrentlyN_ n = flip $ pooledMapConcurrentlyN_ n
+
+-- | 'UnliftIO.Async.pooledForConcurrently_' but passing the thread context along
+pooledForConcurrently_
+  :: (MonadUnliftIO m, MonadMask m, Traversable t) => t a -> (a -> m b) -> m ()
+pooledForConcurrently_ = flip pooledMapConcurrently_
+
 -- | 'UnliftIO.Async.mapConcurrently' but passing the thread context along
 mapConcurrently
   :: (MonadUnliftIO m, MonadMask m, Traversable t) => (a -> m b) -> t a -> m (t b)
@@ -55,10 +108,10 @@
   context <- getThreadContext
   UnliftIO.mapConcurrently (withThreadContext context . f) xs
 
--- | 'UnliftIO.Async.forConcurrently_' but passing the thread context along
-forConcurrently_
-  :: (MonadUnliftIO m, MonadMask m, Traversable t) => t a -> (a -> m b) -> m ()
-forConcurrently_ = flip mapConcurrently_
+-- | 'UnliftIO.Async.forConcurrently' but passing the thread context along
+forConcurrently
+  :: (MonadUnliftIO m, MonadMask m, Traversable t) => t a -> (a -> m b) -> m (t b)
+forConcurrently = flip mapConcurrently
 
 -- | 'UnliftIO.Async.mapConcurrently_' but passing the thread context along
 mapConcurrently_
@@ -66,6 +119,20 @@
 mapConcurrently_ f xs = do
   context <- getThreadContext
   UnliftIO.mapConcurrently_ (withThreadContext context . f) xs
+
+-- | 'UnliftIO.Async.forConcurrently_' but passing the thread context along
+forConcurrently_
+  :: (MonadUnliftIO m, MonadMask m, Traversable t) => t a -> (a -> m b) -> m ()
+forConcurrently_ = flip mapConcurrently_
+
+-- | Run a list of actions concurrently, combining the results monoidally
+--
+-- The forked threads will have the current thread context copied to them.
+foldConcurrently
+  :: (MonadUnliftIO m, MonadMask m, Monoid a, Foldable t) => t (m a) -> m a
+foldConcurrently xs = do
+  context <- getThreadContext
+  runConc $ foldMap (conc . withThreadContext context) xs
 
 -- | Wrapper around creating "Control.Immortal" processes
 --
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
 name: freckle-app
-version: 1.23.0.1
+version: 1.23.1.0
 
 maintainer: Freckle Education
 category: Utils
