diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 # Changelog
 
+## 0.2.2.0
+
+### Bug Fixes
+
+- Make initdb cache creation atomic by copying into a same-parent temporary
+  directory and publishing it with an atomic rename. Concurrent cold-cache
+  writers now treat an existing final cache as success instead of exposing a
+  partially-copied `data` directory.
+
 ## 0.2.1.0
 
 ### Bug Fixes
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -158,6 +158,16 @@
   pure ()
 ```
 
+### Suite-level fixtures with template databases
+
+For larger integration suites where many examples need the same migrated
+schema, consider starting one cached PostgreSQL server for the whole suite,
+migrating a template database once, and cloning clean per-example databases
+with PostgreSQL's `CREATE DATABASE ... TEMPLATE ...`.
+
+See [Suite-level template databases](docs/suite-template-databases.md) for the
+full pattern, tradeoffs, and a complete fixture implementation.
+
 ### Manual Lifecycle Management
 
 For more control, use `start` and `stop` directly:
@@ -430,6 +440,61 @@
 
 Ensure you have write access to the temporary directory. By default, the
 system temp directory is used.
+
+## Tracing
+
+OpenTelemetry tracing is available through the optional companion
+package `ephemeral-pg-opentelemetry`, which lives next to this library
+in the same repository. It mirrors the `EphemeralPg` lifecycle API and
+emits one span per public operation:
+
+| Wrapper                      | Span name                       |
+| ---------------------------- | ------------------------------- |
+| `withTraced`                 | `ephemeralpg.with`              |
+| `withCachedTraced`           | `ephemeralpg.with_cached`       |
+| `startTraced`                | `ephemeralpg.start`             |
+| `stopTraced`                 | `ephemeralpg.stop`              |
+| `restartTraced`              | `ephemeralpg.restart`           |
+| `createSnapshotTraced`       | `ephemeralpg.snapshot.create`   |
+| `restoreSnapshotTraced`      | `ephemeralpg.snapshot.restore`  |
+| `deleteSnapshotTraced`       | `ephemeralpg.snapshot.delete`   |
+| `dumpTraced`                 | `ephemeralpg.dump`              |
+| `restoreTraced`              | `ephemeralpg.restore`           |
+
+Each span carries the standard database attributes
+(`db.system.name`/`db.system`, `db.namespace`/`db.name`) plus
+library-specific ones (`ephemeralpg.port`,
+`ephemeralpg.shutdown.mode`). Attribute name selection obeys
+`OTEL_SEMCONV_STABILITY_OPT_IN` exactly the way upstream HTTP
+instrumentation does — set it to `http` for stable names, `http/dup`
+for both stable and legacy. Errors from `EphemeralPg.start` are
+recorded uniformly with `error.type` (constructor name), span status
+`Error`, and a `recordException` event.
+
+The wrappers are designed to nest under a parent test span. Combined
+with `hs-opentelemetry-instrumentation-hspec`, a test that calls
+`withTraced` produces a tree of the form:
+
+    Run tests
+      └─ ephemeral-pg under OpenTelemetry
+         └─ emits a span tree under withTraced
+            └─ ephemeralpg.with
+               ├─ ephemeralpg.start
+               ├─ ephemeralpg.with.body
+               └─ ephemeralpg.stop
+
+Add the dependency to your test-suite (the package is not yet on
+Hackage; reference it via the local `cabal.project`):
+
+```cabal
+build-depends:
+  ephemeral-pg-opentelemetry,
+  hs-opentelemetry-api,
+  hs-opentelemetry-sdk,
+  hs-opentelemetry-instrumentation-hspec,
+```
+
+See `ephemeral-pg-opentelemetry/test/Demo.hs` for a runnable example.
 
 ## License
 
diff --git a/ephemeral-pg.cabal b/ephemeral-pg.cabal
--- a/ephemeral-pg.cabal
+++ b/ephemeral-pg.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: ephemeral-pg
-version: 0.2.1.0
+version: 0.2.2.0
 synopsis: Temporary PostgreSQL databases for testing
 description:
   A modern library for creating temporary PostgreSQL instances for testing.
@@ -84,18 +84,52 @@
   import: warnings
   default-language: GHC2021
   type: exitcode-stdio-1.0
-  hs-source-dirs: test
+  hs-source-dirs:
+    test
+    src
+
   main-is: Main.hs
+  other-modules:
+    EphemeralPg
+    EphemeralPg.Config
+    EphemeralPg.Database
+    EphemeralPg.Error
+    EphemeralPg.Internal.Cache
+    EphemeralPg.Internal.CopyOnWrite
+    EphemeralPg.Internal.Directory
+    EphemeralPg.Internal.Except
+    EphemeralPg.Internal.Port
+    EphemeralPg.Process
+    EphemeralPg.Process.CreateDb
+    EphemeralPg.Process.InitDb
+    EphemeralPg.Process.Postgres
+
   build-depends:
     QuickCheck >=2.14 && <2.16,
     base,
+    bytestring,
+    directory,
     ephemeral-pg,
+    filepath,
+    hashable,
     hasql,
     hspec >=2.11 && <2.12,
+    network,
+    process,
+    temporary,
     text,
+    transformers,
+    typed-process,
+    unix,
 
   default-extensions:
     BlockArguments
+    DeriveAnyClass
+    DerivingStrategies
     DuplicateRecordFields
+    LambdaCase
+    NoFieldSelectors
     OverloadedRecordDot
     OverloadedStrings
+    RecordWildCards
+    StrictData
diff --git a/src/EphemeralPg/Internal/Cache.hs b/src/EphemeralPg/Internal/Cache.hs
--- a/src/EphemeralPg/Internal/Cache.hs
+++ b/src/EphemeralPg/Internal/Cache.hs
@@ -42,6 +42,7 @@
 import Data.Text (Text)
 import Data.Text qualified as T
 import Data.Text.Encoding qualified as T
+import Data.Unique (hashUnique, newUnique)
 import EphemeralPg.Config (Config (..))
 import EphemeralPg.Internal.CopyOnWrite
   ( CowCapability (..),
@@ -57,9 +58,11 @@
     listDirectory,
     removeDirectoryRecursive,
     removeFile,
+    renamePath,
   )
 import System.Exit (ExitCode (..))
 import System.FilePath ((</>))
+import System.Posix.Process (getProcessID)
 import System.Process.Typed (byteStringOutput, proc, readProcess, setStderr, setStdout)
 
 -- | A cache key uniquely identifies a cached initdb cluster.
@@ -170,16 +173,42 @@
 
 -- | Create a cache from an initialized data directory.
 createCache :: CacheKey -> FilePath -> Maybe FilePath -> IO (Either Text ())
-createCache key srcDataDir mRoot = do
-  dir <- getCacheDirectory key mRoot
-  createDirectoryIfMissing True dir
+createCache key srcDataDir mRoot = runExceptT $ do
+  dir <- liftIO $ getCacheDirectory key mRoot
+  liftIO $ createDirectoryIfMissing True dir
   let dstDataDir = dir </> "data"
 
-  -- Detect CoW capability
-  cowCapability <- detectCowCapability dir
+  alreadyCached <- liftIO $ doesDirectoryExist dstDataDir
+  unless alreadyCached $ do
+    unique <- liftIO newUnique
+    pid <- liftIO getProcessID
+    let tmpDataDir =
+          dir </> ("data.tmp-" <> show pid <> "-" <> show (hashUnique unique))
 
-  -- Copy the data directory to the cache
-  copyDirectory cowCapability srcDataDir dstDataDir
+    cowCapability <- liftIO $ detectCowCapability dir
+    copyResult <- liftIO $ copyDirectory cowCapability srcDataDir tmpDataDir
+    case copyResult of
+      Left err -> do
+        liftIO $ removeDirectoryIfExists tmpDataDir
+        throwE err
+      Right () -> publishCache tmpDataDir dstDataDir
+  where
+    publishCache :: FilePath -> FilePath -> ExceptT Text IO ()
+    publishCache tmpDataDir dstDataDir = do
+      result <- liftIO $ try @SomeException $ renamePath tmpDataDir dstDataDir
+      case result of
+        Right () -> pure ()
+        Left ex -> do
+          winnerExists <- liftIO $ doesDirectoryExist dstDataDir
+          liftIO $ removeDirectoryIfExists tmpDataDir
+          unless winnerExists $
+            throwE $
+              "Failed to publish cache: " <> T.pack (show ex)
+
+    removeDirectoryIfExists :: FilePath -> IO ()
+    removeDirectoryIfExists path = do
+      exists <- doesDirectoryExist path
+      when exists $ removeDirectoryRecursive path
 
 -- | Restore from cache to a new data directory.
 restoreFromCache :: CacheKey -> FilePath -> Maybe FilePath -> IO (Either Text ())
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,9 +1,20 @@
 module Main where
 
+import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar)
+import Control.Monad (forM_, replicateM)
 import Data.Text qualified as T
 import EphemeralPg qualified as Pg
 import EphemeralPg.Config qualified as Config
+import EphemeralPg.Internal.Cache
+  ( CacheKey (..),
+    createCache,
+    getCacheDirectory,
+    restoreFromCache,
+  )
 import Hasql.Connection qualified as Connection
+import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist)
+import System.FilePath ((</>))
+import System.IO.Temp (withSystemTempDirectory)
 import Test.Hspec
 import Test.QuickCheck
 
@@ -74,6 +85,31 @@
             Connection.release conn
             pure ()
       result2 `shouldSatisfy` isRight
+
+    it "publishes concurrently-created caches atomically" $ do
+      withSystemTempDirectory "ephemeral-pg-cache-race-" $ \root -> do
+        let key = CacheKey {pgVersion = "test", configHash = "atomic"}
+            sourceDir = root </> "source-data"
+            restoreDir = root </> "restore-data"
+
+        createDirectoryIfMissing True (sourceDir </> "base")
+        writeFile (sourceDir </> "PG_VERSION") "test\n"
+        writeFile (sourceDir </> "base" </> "marker") "ok\n"
+
+        done <- replicateM 8 newEmptyMVar
+        forM_ done $ \var ->
+          forkIO (createCache key sourceDir (Just root) >>= putMVar var)
+        results <- traverse takeMVar done
+        results `shouldSatisfy` all isRight
+
+        cacheDir <- getCacheDirectory key (Just root)
+        doesFileExist (cacheDir </> "data" </> "PG_VERSION") `shouldReturn` True
+        doesFileExist (cacheDir </> "data" </> "base" </> "marker") `shouldReturn` True
+        doesDirectoryExist (cacheDir </> "data" </> "source-data") `shouldReturn` False
+
+        restoreResult <- restoreFromCache key restoreDir (Just root)
+        restoreResult `shouldSatisfy` isRight
+        doesFileExist (restoreDir </> "PG_VERSION") `shouldReturn` True
 
   describe "Config" $ do
     it "satisfies left identity (mempty <> x = x)" $
