diff --git a/hasql.cabal b/hasql.cabal
--- a/hasql.cabal
+++ b/hasql.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: hasql
-version: 1.10.3
+version: 1.10.3.1
 category: Hasql, Database, PostgreSQL
 synopsis: Fast PostgreSQL driver with a flexible mapping API
 description:
@@ -358,11 +358,11 @@
     hspec-discover:hspec-discover ^>=2.11.12
 
   build-depends:
-    QuickCheck,
     aeson,
     hasql,
     hspec ^>=2.11.12,
     iproute,
+    QuickCheck,
     quickcheck-instances >=0.3.11 && <0.4,
     random >=1.3 && <1.4,
     random ^>=1.3.1,
diff --git a/src/library-tests/Sharing/ByFeature/PreparedStatementCacheSpec.hs b/src/library-tests/Sharing/ByFeature/PreparedStatementCacheSpec.hs
--- a/src/library-tests/Sharing/ByFeature/PreparedStatementCacheSpec.hs
+++ b/src/library-tests/Sharing/ByFeature/PreparedStatementCacheSpec.hs
@@ -3,6 +3,7 @@
 import Data.Either
 import Hasql.Connection qualified as Connection
 import Hasql.Decoders qualified as Decoders
+import Hasql.Errors qualified as Errors
 import Hasql.Pipeline qualified as Pipeline
 import Hasql.Session qualified as Session
 import Hasql.Statement qualified as Statement
@@ -41,6 +42,29 @@
           Left result ->
             expectationFailure ("Unexpected error: " <> show result)
 
+    it "Syntax errors in prepared statements don't corrupt the cache for subsequent uses of the same statement" \config -> do
+      Scripts.onPreparableConnection config \connection -> do
+        let brokenStatement =
+              Statement.preparable
+                "S"
+                mempty
+                Decoders.noResult
+        -- First run: syntax error.
+        result1 <- Connection.use connection do
+          Session.statement () brokenStatement
+        error1 <- case result1 of
+          Left error1 -> pure error1
+          Right _ -> fail "First run unexpectedly succeeded"
+
+        -- Second run of the same statement: should also produce a syntax error,
+        -- not "prepared statement does not exist".
+        result2 <- Connection.use connection do
+          Session.statement () brokenStatement
+        error2 <- case result2 of
+          Left error2 -> pure error2
+          Right _ -> fail "Second run unexpectedly succeeded"
+        shouldBe error2 error1
+
   describe "Pipeline" do
     it "Failing pipeline statements don't cause misses in updates of the prepared statement cache" \config -> do
       Scripts.onPreparableConnection config \connection -> do
@@ -76,3 +100,103 @@
             pure ()
           Left result ->
             expectationFailure ("Unexpected error: " <> show result)
+
+    it "Syntax errors in pipeline prepared statements don't corrupt the cache for subsequent uses of the same statement" \config -> do
+      Scripts.onPreparableConnection config \connection -> do
+        let brokenStatement =
+              Statement.preparable
+                "S"
+                mempty
+                Decoders.noResult
+        -- First run: syntax error.
+        result1 <- Connection.use connection do
+          Session.pipeline (Pipeline.statement () brokenStatement)
+        shouldBe (isLeft result1) True
+        -- Second run of the same statement: should also produce a syntax error,
+        -- not "prepared statement does not exist".
+        result2 <- Connection.use connection do
+          Session.pipeline (Pipeline.statement () brokenStatement)
+        case result2 of
+          Left (Errors.StatementSessionError _ _ _ _ _ (Errors.ServerStatementError (Errors.ServerError "42601" _ _ _ _))) ->
+            pure ()
+          Left other ->
+            expectationFailure ("Unexpected error on second run: " <> show other)
+          Right _ ->
+            expectationFailure "Second run unexpectedly succeeded"
+
+    it "A pipeline with successful statements followed by a broken one can be retried without 'already exists' errors" \config -> do
+      Scripts.onPreparableConnection config \connection -> do
+        let ok1 = Statement.preparable "select 1" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
+            ok2 = Statement.preparable "select 2" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
+            broken = Statement.preparable "S" mempty Decoders.noResult
+        -- First run: pipeline with two OK statements and a broken one at the end.
+        result1 <- Connection.use connection do
+          Session.pipeline do
+            (,,)
+              <$> Pipeline.statement () ok1
+              <*> Pipeline.statement () ok2
+              <*> Pipeline.statement () broken
+        error1 <- case result1 of
+          Left error1 -> pure error1
+          Right _ -> fail "First run unexpectedly succeeded"
+
+        -- Second run of the same pipeline: must fail with the SAME syntax error,
+        -- not "prepared statement already exists".
+        result2 <- Connection.use connection do
+          Session.pipeline do
+            (,,)
+              <$> Pipeline.statement () ok1
+              <*> Pipeline.statement () ok2
+              <*> Pipeline.statement () broken
+        error2 <- case result2 of
+          Left error2 -> pure error2
+          Right _ -> fail "Second run unexpectedly succeeded"
+        shouldBe error2 error1
+
+        -- Also, a standalone valid statement should still work afterwards.
+        result3 <- Connection.use connection do
+          Session.statement () ok1
+        case result3 of
+          Right val -> val `shouldBe` 1
+          Left err -> expectationFailure ("Unexpected error on standalone statement: " <> show err)
+
+    it "A pipeline with a broken statement in the middle can be retried without 'already exists' errors" \config -> do
+      Scripts.onPreparableConnection config \connection -> do
+        let ok1 = Statement.preparable "select 1" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
+            broken = Statement.preparable "S" mempty Decoders.noResult
+            ok2 = Statement.preparable "select 2" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
+        -- First run: pipeline with broken statement in the middle.
+        result1 <- Connection.use connection do
+          Session.pipeline do
+            (,,)
+              <$> Pipeline.statement () ok1
+              <*> Pipeline.statement () broken
+              <*> Pipeline.statement () ok2
+        shouldBe (isLeft result1) True
+
+        -- Second run of the same pipeline: must fail with the same syntax error.
+        result2 <- Connection.use connection do
+          Session.pipeline do
+            (,,)
+              <$> Pipeline.statement () ok1
+              <*> Pipeline.statement () broken
+              <*> Pipeline.statement () ok2
+        case result2 of
+          Left (Errors.StatementSessionError _ _ _ _ _ (Errors.ServerStatementError (Errors.ServerError "42601" _ _ _ _))) ->
+            pure ()
+          Left other ->
+            expectationFailure ("Unexpected error on second run: " <> show other)
+          Right _ ->
+            expectationFailure "Second run unexpectedly succeeded"
+
+        -- Standalone valid statements should still work afterwards.
+        result3 <- Connection.use connection do
+          Session.statement () ok1
+        case result3 of
+          Right val -> val `shouldBe` 1
+          Left err -> expectationFailure ("Unexpected error on standalone ok1: " <> show err)
+        result4 <- Connection.use connection do
+          Session.statement () ok2
+        case result4 of
+          Right val -> val `shouldBe` 2
+          Left err -> expectationFailure ("Unexpected error on standalone ok2: " <> show err)
diff --git a/src/library/Hasql/Engine/Contexts/Pipeline.hs b/src/library/Hasql/Engine/Contexts/Pipeline.hs
--- a/src/library/Hasql/Engine/Contexts/Pipeline.hs
+++ b/src/library/Hasql/Engine/Contexts/Pipeline.hs
@@ -62,7 +62,11 @@
                         )
                     )
 
-          pure (result, newOidCache, newStatementCache)
+          let finalStatementCache = case result of
+                Left _ -> StatementCache.revertTo statementCache newStatementCache
+                Right _ -> newStatementCache
+
+          pure (result, newOidCache, finalStatementCache)
 
 -- |
 -- Composable abstraction over the execution of queries in [the pipeline mode](https://www.postgresql.org/docs/current/libpq-pipeline-mode.html).
diff --git a/src/library/Hasql/Engine/Structures/StatementCache.hs b/src/library/Hasql/Engine/Structures/StatementCache.hs
--- a/src/library/Hasql/Engine/Structures/StatementCache.hs
+++ b/src/library/Hasql/Engine/Structures/StatementCache.hs
@@ -5,6 +5,7 @@
     lookup,
     insert,
     reset,
+    revertTo,
   )
 where
 
@@ -43,6 +44,16 @@
 {-# INLINEABLE reset #-}
 reset :: StatementCache -> StatementCache
 reset _ = StatementCache HashMap.empty 0
+
+-- | Revert the entries to those of the provided cache, preserving the counter of the receiver.
+--
+-- This is useful when a pipeline fails and we want to discard tentative entries
+-- (which might not have been successfully prepared on the server)
+-- while keeping the counter advanced to avoid name collisions with orphaned server-side statements.
+{-# INLINEABLE revertTo #-}
+revertTo :: StatementCache -> StatementCache -> StatementCache
+revertTo (StatementCache oldHashMap _) (StatementCache _ newCounter) =
+  StatementCache oldHashMap newCounter
 
 -- |
 -- Local statement key.
