diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## [0.15.0.2] - 2026-05-15
+
+### Changed
+
+* Fixed an exponential-time blowup when parsing inputs with several `many`-style
+  options combined with a `choice` containing positional arguments. Parses that
+  previously took minutes now complete in milliseconds. The fix makes the
+  internal `ListT` nondet stream lazy in its second argument and threads inner
+  branches into the outer parser on demand.
+
 ## [0.15.0.1] - 2026-03-18
 
 ### Changed
diff --git a/opt-env-conf.cabal b/opt-env-conf.cabal
--- a/opt-env-conf.cabal
+++ b/opt-env-conf.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           opt-env-conf
-version:        0.15.0.1
+version:        0.15.0.2
 synopsis:       Settings parsing for Haskell: command-line arguments, environment variables, and configuration values.
 homepage:       https://github.com/NorfairKing/opt-env-conf#readme
 bug-reports:    https://github.com/NorfairKing/opt-env-conf/issues
diff --git a/src/OptEnvConf/NonDet.hs b/src/OptEnvConf/NonDet.hs
--- a/src/OptEnvConf/NonDet.hs
+++ b/src/OptEnvConf/NonDet.hs
@@ -56,12 +56,16 @@
 joinMMMList :: (Monad m) => m (MList m (m (MList m a))) -> m (MList m a)
 joinMMMList = (>>= joinMMList)
 
-appendMList :: (Functor m) => MList m a -> MList m a -> MList m a
-appendMList MNil ml = ml
-appendMList (MCons a ml1) ml2 = MCons a $ (`appendMList` ml2) <$> ml1
-
-appendMMMList :: (Applicative m) => m (MList m a) -> m (MList m a) -> m (MList m a)
-appendMMMList ml1 ml2 = appendMList <$> ml1 <*> ml2
+-- Lazy in the second argument: do not run its effects until the first list
+-- is exhausted. A strict definition (e.g. liftA2 over an eager concat) forces
+-- the entire downstream search tree just to produce the head element, which
+-- causes exponential blowup through ListT's >>= and <|>.
+appendMMMList :: (Monad m) => m (MList m a) -> m (MList m a) -> m (MList m a)
+appendMMMList ml1 ml2 = do
+  l1 <- ml1
+  case l1 of
+    MNil -> ml2
+    MCons a m -> pure $ MCons a (appendMMMList m ml2)
 
 -- This can be directly used as a monad transformer
 newtype ListT m a = ListT {unListT :: m (MList m a)}
diff --git a/src/OptEnvConf/Run.hs b/src/OptEnvConf/Run.hs
--- a/src/OptEnvConf/Run.hs
+++ b/src/OptEnvConf/Run.hs
@@ -152,8 +152,7 @@
         ppIndent $ do
           e <- ask
           s <- get
-          results <- liftIO $ runPP (go p') s e
-          (result, s') <- ppNonDetList results
+          (result, s') <- ppNonDet $ runPPNonDet (go p') s e
           put s'
           case result of
             Success a -> pure a
@@ -606,13 +605,15 @@
       MonadState PPState
     )
 
-runPP ::
+-- Keeps nondeterminism lazy: callers consume the resulting branches one at a
+-- time, so unreached branches never run.
+runPPNonDet ::
   PP a ->
   PPState ->
   PPEnv ->
-  IO [(Validation ParseError a, PPState)]
-runPP (PP p) args envVars =
-  runNonDetT (runStateT (runValidationT (runReaderT p envVars)) args)
+  NonDetT IO (Validation ParseError a, PPState)
+runPPNonDet (PP p) args envVars =
+  runStateT (runValidationT (runReaderT p envVars)) args
 
 runPPLazy ::
   PP a ->
@@ -631,13 +632,11 @@
 tryPP pp = do
   s <- get
   e <- ask
-  results <- liftIO $ runPP pp s e
-  (errOrRes, s') <- ppNonDetList results
+  (errOrRes, s') <- ppNonDet $ runPPNonDet pp s e
   case errOrRes of
     Failure errs ->
       if all errorIsForgivable errs
-        then do
-          pure Nothing
+        then pure Nothing
         else ppErrors' errs
     Success a -> do
       put s' -- Only set state if parsing succeeded.
