diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
 # Revision history for blockio-uring
 
+## 0.2.0.0 -- 2026-04-30
+
+### Breaking changes
+
+* `IOCtxParams` has a new record field for configuring the use of IOWAIT metrics
+  (see below).
+
+### New features
+
+* Support enabling/disabling IOWAIT metrics. See [issue
+  #55](https://github.com/well-typed/blockio-uring/issues/55) and [PR
+  #57](https://github.com/well-typed/blockio-uring/pull/57).
+
+### Minor changes
+
+None
+
+### Bug fixes
+
+* On systems with limited CPU and memory resources, `submitIO` could reliably
+  fail with `EFAULT` error numbers because of frequent reaping and rescheduling
+  of Haskell's lightweight threads. The bug is fixed by always running
+  `submitIO` in a fresh bound thread. See [issue
+  #58](https://github.com/well-typed/blockio-uring/issues/58) and [PR
+  #60](https://github.com/well-typed/blockio-uring/pull/60).
+
 ## 0.1.0.3 -- 2026-03-12
 
 * PATCH: support `ghc-9.14`. See [PR
diff --git a/benchmark/Bench.hs b/benchmark/Bench.hs
--- a/benchmark/Bench.hs
+++ b/benchmark/Bench.hs
@@ -127,7 +127,8 @@
       lastBlock = fromIntegral (size `div` 4096 - 1)
       params    = IOCtxParams {
                     ioctxBatchSizeLimit   = 64,
-                    ioctxConcurrencyLimit = 64 * 4
+                    ioctxConcurrencyLimit = 64 * 4,
+                    ioctxIOWaitMetrics    = True
                   }
       ntasks    = 4 * ncaps
       batchsz   = 32
@@ -243,4 +244,3 @@
         (i, rng') = uniformR (0, Set.size xs - 1) rng
         !x   = Set.elemAt i xs
         !xs' = Set.deleteAt i xs
-
diff --git a/blockio-uring.cabal b/blockio-uring.cabal
--- a/blockio-uring.cabal
+++ b/blockio-uring.cabal
@@ -1,6 +1,6 @@
 cabal-version:   3.4
 name:            blockio-uring
-version:         0.1.0.3
+version:         0.2.0.0
 synopsis:        Perform batches of asynchronous disk IO operations.
 description:
   This library supports disk I/O operations using the Linux io_uring API. The
@@ -24,7 +24,9 @@
 copyright:       (c) Well-Typed LLP 2022 - 2025
 category:        System
 build-type:      Simple
-tested-with:     GHC ==9.2 || ==9.4 || ==9.6 || ==9.8 || ==9.10 || ==9.12 || ==9.14
+tested-with:
+  GHC ==9.2 || ==9.4 || ==9.6 || ==9.8 || ==9.10 || ==9.12 || ==9.14
+
 extra-doc-files:
   CHANGELOG.md
   README.md
@@ -36,7 +38,7 @@
 source-repository this
   type:     git
   location: https://github.com/well-typed/blockio-uring
-  tag:      blockio-uring-0.1.0.3
+  tag:      blockio-uring-0.2.0.0
 
 common warnings
   ghc-options:
@@ -88,7 +90,7 @@
     , base        <5
     , containers
     , primitive
-    , random ^>= 1.3
+    , random      ^>=1.3
     , time
     , unix        ^>=2.8.7.0
     , vector
diff --git a/src/System/IO/BlockIO.hs b/src/System/IO/BlockIO.hs
--- a/src/System/IO/BlockIO.hs
+++ b/src/System/IO/BlockIO.hs
@@ -29,7 +29,7 @@
 import Control.Monad
 import Control.Monad.Primitive
 import Control.Concurrent (forkOn, myThreadId, threadCapability,
-                           getNumCapabilities)
+                           getNumCapabilities, runInBoundThread)
 import Control.Concurrent.MVar
 import Control.Concurrent.QSemN
 import Control.Concurrent.Chan
@@ -122,7 +122,16 @@
     -- If a use of 'submitIO' would lead to this limit being exceeded, then the
     -- call to 'submitIO' will block until enough I/O batches have been
     -- processed.
-    ioctxConcurrencyLimit :: !Int
+    ioctxConcurrencyLimit :: !Int,
+
+    -- | Enable or disable IOWAIT metrics
+    --
+    -- If @io_uring_set_iowait(3)@ is available, then it will be called with
+    -- this value to either enable ('True') or disable ('False') IOWAIT
+    -- metrics. This is innocuous in terms of performance but changes how CPU
+    -- idle time is reported. @io_uring_set_iowait(3)@ is available for
+    -- liburing versions >= 2.10 and Linux kernels versions >= 6.15.
+    ioctxIOWaitMetrics :: !Bool
   }
   deriving stock Show
 
@@ -132,7 +141,8 @@
 defaultIOCtxParams =
   IOCtxParams {
     ioctxBatchSizeLimit   = 64,
-    ioctxConcurrencyLimit = 64 * 3
+    ioctxConcurrencyLimit = 64 * 3,
+    ioctxIOWaitMetrics    = True
   }
 
 validateIOCtxParams :: IOCtxParams -> Maybe String
@@ -179,11 +189,13 @@
 initIOCapCtx :: IOCtxParams -> CapNo -> IO IOCapCtx
 initIOCapCtx IOCtxParams {
                ioctxBatchSizeLimit,
-               ioctxConcurrencyLimit
+               ioctxConcurrencyLimit,
+               ioctxIOWaitMetrics
              } capno = do
     mask_ $ do
       ioctxQSemN         <- newQSemN ioctxConcurrencyLimit
       uring              <- URing.setupURing (URing.URingParams ioctxBatchSizeLimit ioctxConcurrencyLimit)
+      URing.setIOWait uring ioctxIOWaitMetrics
       ioctxURing         <- newMVar (Just uring)
       ioctxChanIOBatch   <- newChan
       ioctxChanIOBatchIx <- newChan
@@ -284,16 +296,24 @@
 --   at least the target number in flight at once.
 --
 submitIO :: IOCtx -> V.Vector (IOOp RealWorld) -> IO (VU.Vector IOResult)
-submitIO (IOCtx capctxs) !ioops = do
-    -- Find out which capability the thread is currently running on and use
-    -- that one. It does _not matter_ for correctness if the thread is migrated
-    -- while the I/O is submitted or when waiting for completion. Migration
-    -- happens sufficiently infrequently that it should not be a performance
-    -- problem.
-    tid <- myThreadId
-    (capno, _) <- threadCapability tid
-    let !capctx = capctxs V.! (capno `mod` V.length capctxs)
-    submitCapIO capctx ioops
+submitIO (IOCtx capctxs) !ioops =
+    -- Requests have to be submitted from within a bound thread, or we might get
+    -- @EFAULT@ errors. See issue #58.
+    --
+    -- TODO <https://github.com/well-typed/blockio-uring/issues/61>: ideally,
+    -- for performance, it would be better to run a smaller section of the
+    -- 'submitIO' code inside 'runInBoundThread'. However, it is not 100% clear
+    -- what the critical code section is that /has/ to run in a bound thread .
+    -- So, for now we pick the safe option of running the entirety of 'submitIO'
+    -- in a bound thread.
+    runInBoundThread $ do
+      -- Find out which capability the thread is currently running on and use
+      -- that one. It does _not matter_ for correctness that the thread submits
+      -- IO to that same thread's capability's ring, but it is more performant.
+      tid <- myThreadId
+      (capno, _) <- threadCapability tid
+      let !capctx = capctxs V.! (capno `mod` V.length capctxs)
+      submitCapIO capctx ioops
 
 submitCapIO :: IOCapCtx -> V.Vector (IOOp RealWorld) -> IO (VU.Vector IOResult)
 submitCapIO ioctx@IOCapCtx {ioctxBatchSizeLimit'} !ioops
diff --git a/src/System/IO/BlockIO/URing.hs b/src/System/IO/BlockIO/URing.hs
--- a/src/System/IO/BlockIO/URing.hs
+++ b/src/System/IO/BlockIO/URing.hs
@@ -7,6 +7,7 @@
     setupURing,
     closeURing,
     withURing,
+    setIOWait,
     IOOpId(..),
     prepareRead,
     prepareWrite,
@@ -94,6 +95,10 @@
 withURing params =
     bracket (setupURing params) closeURing
 
+setIOWait :: URing -> Bool -> IO ()
+setIOWait URing {uringptr} b =
+    callIfSupported_ "setIOWAIT" $
+      FFI.io_uring_set_iowait uringptr (if b then 1 else 0)
 
 --
 -- Submitting I/O
@@ -231,6 +236,19 @@
                    (Errno (-res))
                    Nothing Nothing
 
+callIfSupported_ :: String -> IO CInt -> IO ()
+callIfSupported_ label action = do
+  res <- action
+  if Errno (-res) == eOPNOTSUPP
+    then pure ()
+    else
+      when (res < 0) $
+        throwIO $
+          errnoToIOError
+          label
+          (Errno (-res))
+          Nothing Nothing
+
 throwErrResIfNull :: String -> IOErrorType -> String -> IO (Ptr a) -> IO (Ptr a)
 throwErrResIfNull location ioErrorType description action = do
     res <- action
@@ -243,4 +261,3 @@
                  Nothing Nothing)
               description
       else return res
-
diff --git a/src/System/IO/BlockIO/URingFFI.hsc b/src/System/IO/BlockIO/URingFFI.hsc
--- a/src/System/IO/BlockIO/URingFFI.hsc
+++ b/src/System/IO/BlockIO/URingFFI.hsc
@@ -125,3 +125,20 @@
 foreign import capi unsafe "liburing.h io_uring_cqe_seen"
   io_uring_cqe_seen :: Ptr URing -> Ptr URingCQE -> IO ()
 
+
+--
+-- Calling @io_uring_set_iowait(3)@ if available
+--
+
+-- | Available only on liburing >= 2.10 and kernel >= 6.15.
+--
+-- See @io_uring_set_iowait(3)@ man page.
+#ifdef IORING_FEAT_NO_IOWAIT
+#if IORING_FEAT_NO_IOWAIT
+foreign import capi unsafe "liburing.h io_uring_set_iowait"
+  io_uring_set_iowait :: Ptr URing -> CBool -> IO CInt
+#endif
+#else
+io_uring_set_iowait :: Ptr URing -> CBool -> IO CInt
+io_uring_set_iowait _ _ = pure #{const EOPNOTSUPP}
+#endif
diff --git a/test/test.hs b/test/test.hs
--- a/test/test.hs
+++ b/test/test.hs
@@ -98,9 +98,9 @@
     concurrencyLimitBoundsExcl batchSizeLimit =  (batchSizeLimit, 2^(16::Int))
 
 instance Arbitrary IOCtxParams where
-  arbitrary = IOCtxParams <$> genLimit <*> genLimit
-  shrink (IOCtxParams a b) =
-      [ IOCtxParams a' b' | (a', b') <- liftShrink2 shrinkLimit shrinkLimit (a, b) ]
+  arbitrary = IOCtxParams <$> genLimit <*> genLimit <*> arbitrary
+  shrink (IOCtxParams a b i) =
+      [ IOCtxParams a' b' i' | (a', (b', i')) <- liftShrink2 shrinkLimit (liftShrink2 shrinkLimit shrink) (a, (b, i)) ]
 
 genLimit :: Gen Int
 genLimit = frequency [
