diff --git a/bounded-qsem.cabal b/bounded-qsem.cabal
new file mode 100644
--- /dev/null
+++ b/bounded-qsem.cabal
@@ -0,0 +1,21 @@
+name: bounded-qsem
+category: Concurrency
+synopsis: Bounded quantity semaphores.
+description: Bounded quantity semaphores, meaning quantity semaphores that have an
+             upper-bound in the number of units that can be available.
+version: 0.1.0.0
+cabal-version: 1.18
+build-type: Simple
+author: Daniel Casanueva (daniel.casanueva `at` proton.me)
+maintainer: Daniel Casanueva (daniel.casanueva `at` proton.me)
+bug-reports: https://gitlab.com/daniel-casanueva/haskell/bounded-qsem/-/issues
+license: MIT
+license-file: license
+extra-doc-files: readme.md, changelog.md
+
+library
+  default-language: Haskell2010
+  hs-source-dirs: src
+  exposed-modules: Control.Concurrent.BQSem
+  ghc-options: -Wall
+  build-depends: base >= 4.8 && < 4.19
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,2 @@
+## 0.1.0.0
+* First release.
diff --git a/license b/license
new file mode 100644
--- /dev/null
+++ b/license
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Daniel Casanueva
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,4 @@
+# bounded-qsem
+
+Bounded version of the quantity semaphores provided by
+[base](https://hackage.haskell.org/package/base/docs/Control-Concurrent-QSem.html).
diff --git a/src/Control/Concurrent/BQSem.hs b/src/Control/Concurrent/BQSem.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Concurrent/BQSem.hs
@@ -0,0 +1,74 @@
+
+{-# LANGUAGE Safe #-}
+
+-- | Bounded quantity semaphores.
+module Control.Concurrent.BQSem
+  ( BQSem
+  , newBQSem
+  , waitBQSem
+  , signalBQSem
+  , getBQSemQuantity
+    ) where
+
+import Control.Concurrent.QSem
+import Control.Concurrent.MVar
+import Control.Exception (mask, onException)
+import Control.Monad (unless)
+
+-- | Bounded quantity semaphore in which the resource is acquired and released in units of one,
+--   but with a maximum amount of units available at any given time.
+data BQSem = BQSem
+  { -- | Underlying unbounded quantity semaphore.
+    unboundedQSem :: QSem
+    -- | Maximum number of units.
+  , bqsemBound :: Int
+    -- | Counter of current units.
+  , bqsemCounter :: MVar Int
+    }
+
+-- | Build a new 'BQSem' with supplied initial and maximum supply.
+--   An exception is thrown in any of the following cases:
+--
+--   * Initial supply is negative.
+--   * Maximum supply is less than 1.
+--   * Initial supply exceeds maximum.
+--
+newBQSem
+  :: Int -- ^ Initial unit supply.
+  -> Int -- ^ Maximum unit supply.
+  -> IO BQSem
+newBQSem n0 m = do
+  unless (n0 <= m) $ fail "newBQSem: Initial quantity must be less or equal than maximum."
+  unless (m > 0) $ fail "newBQSem: Maximum quantity must be at least 1."
+  qsem <- newQSem n0
+  counter <- newMVar n0
+  pure $ BQSem
+    { unboundedQSem = qsem
+    , bqsemBound = m
+    , bqsemCounter = counter
+      }
+
+-- | Wait for a unit to become available.
+waitBQSem :: BQSem -> IO ()
+waitBQSem bqsem =
+  mask $ \restore -> do
+    restore $ waitQSem $ unboundedQSem bqsem
+    let counter = bqsemCounter bqsem
+    takeMVar counter >>=
+      \n -> putMVar counter $! n - 1
+
+-- | Make a new unit available, unless the maximum number of units has been reached,
+--   in which case it does nothing (it doesn't block).
+signalBQSem :: BQSem -> IO ()
+signalBQSem bqsem =
+  mask $ \restore -> do
+    let counter = bqsemCounter bqsem
+    n <- takeMVar counter
+    if n == bqsemBound bqsem
+       then putMVar counter n
+       else do restore (signalQSem $ unboundedQSem bqsem) `onException` putMVar counter n
+               putMVar counter $! n + 1
+
+-- | Get current supply quantity.
+getBQSemQuantity :: BQSem -> IO Int
+getBQSemQuantity = readMVar . bqsemCounter
