diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,7 @@
+# Changelog for batch
+
+## 0.1.0.0
+
+* First release
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Alexander Thiemann (c) 2018
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Alexander Thiemann nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# batch
+
+[![CircleCI](https://circleci.com/gh/agrafix/batch.svg?style=svg)](https://circleci.com/gh/agrafix/batch)
+
+Simplify queuing up data and processing it in batch.
+
+```haskell
+import Control.Batch
+import Control.Concurrent.STM
+import Control.Monad
+
+example :: IO ()
+example =
+  do outVar <- atomically $ newTVar []
+     let cfg =
+             Batch
+             { b_runEveryItems = Just 5
+             , b_runAfterTimeout = Nothing
+             , b_maxQueueLength = Nothing
+             , b_runBatch =
+                     \x -> atomically $ modifyTVar' outVar (++x)
+             }
+     withBatchRunner cfg $ \hdl ->
+         do replicateM_ 5 $ bh_enqueue hdl True
+            out <-
+                atomically $
+                do x <- readTVar outVar
+                   when (length x /= 5) retry
+                   pure x
+            out `shouldBe` [True, True, True, True, True]
+```
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/batch.cabal b/batch.cabal
new file mode 100644
--- /dev/null
+++ b/batch.cabal
@@ -0,0 +1,64 @@
+-- This file has been generated from package.yaml by hpack version 0.20.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: d87b4be02cf9109e8c9efd7511dbf3c5f169411d9f890206d978a2de89349c21
+
+name:           batch
+version:        0.1.0.0
+synopsis:       Simplify queuing up data and processing it in batch.
+description:    Simplify queuing up data and processing it in batch.
+category:       Data
+homepage:       https://github.com/agrafix/batch#readme
+bug-reports:    https://github.com/agrafix/batch/issues
+author:         Alexander Thiemann
+maintainer:     mail@athiemann.net
+copyright:      2017 Alexander Thiemann <mail@athiemann.net>
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    ChangeLog.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/agrafix/batch
+
+library
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      async >=2.0
+    , base >=4.7 && <5
+    , lifted-async >=0.9
+    , lifted-base >=0.2.3
+    , monad-control >=1.0
+    , mtl >=2.2
+    , stm >=2.4
+    , timespan >=0.3
+    , transformers-base >=0.4
+  exposed-modules:
+      Control.Batch
+  other-modules:
+      Paths_batch
+  default-language: Haskell2010
+
+test-suite batch-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , batch
+    , hspec >=2.4
+    , stm >=2.4
+    , timespan >=0.3
+  other-modules:
+      Paths_batch
+  default-language: Haskell2010
diff --git a/src/Control/Batch.hs b/src/Control/Batch.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Batch.hs
@@ -0,0 +1,102 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE StrictData #-}
+module Control.Batch
+    ( Batch(..)
+    , BatchHandle(..)
+    , withBatchRunner
+    )
+where
+
+import Control.Concurrent.Async
+import Control.Concurrent.STM
+import Control.Exception.Lifted
+import Control.Monad
+import Control.Monad.Base
+import Control.Monad.Trans.Control
+import Data.Time.TimeSpan
+import qualified Control.Concurrent.Async.Lifted as L
+
+data Batch v m
+    = Batch
+    { b_runEveryItems :: Maybe Int
+      -- ^ flush in memory items ever N items for processing
+    , b_runAfterTimeout :: Maybe TimeSpan
+      -- ^ flush after item stuck in queue for longer than this
+    , b_maxQueueLength :: Maybe Int
+      -- ^ if more than N items are enqueued, block enqueueing
+    , b_runBatch :: [v] -> m ()
+      -- ^ function describing how batches should be processed
+    }
+
+data BatchHandle v m
+    = BatchHandle
+    { bh_enqueue :: v -> m ()
+      -- ^ enqueue a new element
+    }
+
+withBatchRunner ::
+    forall v m a. MonadBaseControl IO m
+    => Batch v m -> (BatchHandle v m -> m a) -> m a
+withBatchRunner batch go =
+    do dummy <- liftBase $ async $ pure ()
+       (queueVar, sizeVar, flushThreadVar, triggerVar, stopVar) <-
+           liftBase $ atomically $
+           (,,,,) <$> newTQueue <*> newTVar 0 <*> newTVar dummy <*> newTVar False <*> newTVar False
+       let flusher :: m ()
+           flusher =
+               do shouldStop <-
+                      liftBase $
+                      atomically $
+                      do x <- readTVar triggerVar
+                         stop <- readTVar stopVar
+                         unless (stop || x) retry
+                         writeTVar triggerVar False
+                         pure stop
+                  drain <-
+                      liftBase $
+                      atomically $
+                      do let readLoop !accum =
+                                 do x <- tryReadTQueue queueVar
+                                    case x of
+                                      Nothing -> pure (reverse accum)
+                                      Just v -> readLoop (v : accum)
+                         readLoop []
+                  b_runBatch batch drain
+                  unless shouldStop flusher
+           flush = writeTVar triggerVar True
+           enqueue item =
+               liftBase $
+               do case b_maxQueueLength batch of
+                    Just maxLen ->
+                        atomically $
+                        do totalSize <- readTVar sizeVar
+                           when (totalSize > maxLen) retry
+                    Nothing -> pure ()
+                  waiter <-
+                      async $
+                      case b_runAfterTimeout batch of
+                        Nothing -> pure ()
+                        Just ts ->
+                            do sleepTS ts
+                               atomically flush
+                  (oldFlushThread :: Async ()) <-
+                      atomically $
+                      do writeTQueue queueVar item
+                         modifyTVar' sizeVar (+1)
+                         totalSize <- readTVar sizeVar
+                         oldFlush <- readTVar flushThreadVar
+                         case b_runEveryItems batch of
+                           Just x | totalSize >= x -> flush
+                           _ -> pure ()
+                         writeTVar flushThreadVar waiter
+                         pure oldFlush
+                  uninterruptibleCancel oldFlushThread
+       flushThread <- L.async flusher
+       let bh = BatchHandle enqueue
+           cleanup =
+               liftBase $
+               do atomically $ writeTVar stopVar True >> flush
+                  wait flushThread
+       go bh `finally` cleanup
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,64 @@
+import Control.Batch
+import Control.Concurrent.STM
+import Control.Monad
+import Data.IORef
+import Data.Time.TimeSpan
+
+import Test.Hspec
+
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec =
+    describe "BatchJobs" $
+    do it "batch job is processed on termination" $
+           do outVar <- newIORef []
+              let cfg =
+                      Batch
+                      { b_runEveryItems = Nothing
+                      , b_runAfterTimeout = Nothing
+                      , b_maxQueueLength = Nothing
+                      , b_runBatch = \x -> writeIORef outVar x
+                      }
+              withBatchRunner cfg $ \hdl ->
+                  do bh_enqueue hdl True
+                     bh_enqueue hdl False
+              readIORef outVar `shouldReturn` [True, False]
+       it "batch jobs processes when N items reached" $
+           do outVar <- atomically $ newTVar []
+              let cfg =
+                      Batch
+                      { b_runEveryItems = Just 5
+                      , b_runAfterTimeout = Nothing
+                      , b_maxQueueLength = Nothing
+                      , b_runBatch =
+                              \x -> atomically $ modifyTVar' outVar (++x)
+                      }
+              withBatchRunner cfg $ \hdl ->
+                  do replicateM_ 5 $ bh_enqueue hdl True
+                     out <-
+                         atomically $
+                         do x <- readTVar outVar
+                            when (length x /= 5) retry
+                            pure x
+                     out `shouldBe` [True, True, True, True, True]
+       it "batch jobs processes when timeout reached" $
+           do outVar <- atomically $ newTVar []
+              let cfg =
+                      Batch
+                      { b_runEveryItems = Nothing
+                      , b_runAfterTimeout = Just (seconds 1)
+                      , b_maxQueueLength = Nothing
+                      , b_runBatch =
+                              \x -> atomically $ modifyTVar' outVar (++x)
+                      }
+              withBatchRunner cfg $ \hdl ->
+                  do replicateM_ 5 $ bh_enqueue hdl True
+                     out <-
+                         atomically $
+                         do x <- readTVar outVar
+                            when (length x /= 5) retry
+                            pure x
+                     out `shouldBe` [True, True, True, True, True]
