diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+## Unreleased
+
+## 0.1.0.0 - 2026-06-13
+
+### Added
+
+- Initial Hackage release of the Redis backend for shikumi-cache.
+- Redis connection lifecycle helpers, configurable TTL support, and a cache interpreter for the shikumi cache effect.
diff --git a/shikumi-cache-redis.cabal b/shikumi-cache-redis.cabal
new file mode 100644
--- /dev/null
+++ b/shikumi-cache-redis.cabal
@@ -0,0 +1,67 @@
+cabal-version:   3.4
+name:            shikumi-cache-redis
+version:         0.1.0.0
+synopsis:        Redis-backed shikumi cache (EP-6)
+category:        AI
+description:
+  A Redis backend for shikumi's @Cache@ effect (EP-6), kept in a separate
+  package so the heavy @hedis@ client and a running Redis server are only
+  pulled in when actually used. Entries are stored under the string key
+  @shikumi:cache:\<hex\>@ with a configurable TTL; the value is the UTF-8 JSON of
+  a @CachedResponse@ (the @Response@ round-trip comes from @shikumi-cache@'s
+  @Shikumi.Cache.ResponseJSON@).
+
+license:         BSD-3-Clause
+author:          Nadeem Bitar
+maintainer:      nadeem@gmail.com
+build-type:      Simple
+extra-doc-files: CHANGELOG.md
+
+common common-options
+  ghc-options:
+    -Wall -Wcompat -Widentities -Wincomplete-uni-patterns
+    -Wincomplete-record-updates -Wredundant-constraints
+    -fhide-source-paths -Wmissing-export-lists -Wpartial-fields
+    -Wmissing-deriving-strategies
+
+  default-language:   GHC2024
+  default-extensions:
+    DeriveAnyClass
+    DuplicateRecordFields
+    OverloadedLabels
+    OverloadedStrings
+
+library
+  import:          common-options
+  hs-source-dirs:  src
+  exposed-modules: Shikumi.Cache.Backend.Redis
+  build-depends:
+    , aeson
+    , base           >=4.20     && <5
+    , bytestring
+    , effectful
+    , hedis
+    , shikumi-cache  ^>=0.1.0.0
+    , text           ^>=2.1
+
+test-suite shikumi-cache-redis-test
+  import:         common-options
+  type:           exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is:        Main.hs
+  ghc-options:    -threaded -with-rtsopts=-N
+  build-depends:
+    , baikai
+    , base
+    , bytestring
+    , effectful
+    , generic-lens
+    , hedis
+    , lens
+    , shikumi              ^>=0.1.0.0
+    , shikumi-cache        ^>=0.1.0.0
+    , shikumi-cache-redis  ^>=0.1.0.0
+    , tasty
+    , tasty-hunit
+    , text
+    , vector
diff --git a/src/Shikumi/Cache/Backend/Redis.hs b/src/Shikumi/Cache/Backend/Redis.hs
new file mode 100644
--- /dev/null
+++ b/src/Shikumi/Cache/Backend/Redis.hs
@@ -0,0 +1,72 @@
+-- | The Redis cache backend (EP-6) — a server-backed, cross-machine store.
+--
+-- Unlike the in-memory (process-local) and SQLite (single-file) backends, a
+-- Redis-backed cache is shared by every process that can reach the server, so a
+-- response computed on one machine is served to another. Entries live under the
+-- string key @shikumi:cache:\<hex\>@ (the @shikumi:cache:@ prefix is operational
+-- namespacing, outside the content hash) and are written with @SETEX@ so Redis
+-- evicts stale entries after a configurable TTL. The value is the UTF-8 JSON of
+-- a 'CachedResponse'; the baikai 'Baikai.Response.Response' round-trip is the one
+-- from @shikumi-cache@ ("Shikumi.Cache.ResponseJSON").
+module Shikumi.Cache.Backend.Redis
+  ( RedisCache,
+    defaultRedisTTL,
+    openRedisCache,
+    openRedisCacheWithTTL,
+    closeRedisCache,
+    runCacheRedis,
+  )
+where
+
+import Data.Aeson (decodeStrict', encode)
+import Data.ByteString (ByteString)
+import Data.ByteString.Lazy qualified as BL
+import Data.Text.Encoding qualified as TE
+import Database.Redis qualified as R
+import Effectful (Eff, IOE, liftIO, (:>))
+import Effectful.Dispatch.Dynamic (interpret)
+import Shikumi.Cache (Cache (..), CacheKey (unCacheKey))
+
+-- | An open Redis-backed cache: a connection plus the TTL (seconds) applied to
+-- every stored entry.
+data RedisCache = RedisCache
+  { redisConn :: !R.Connection,
+    redisTTL :: !Integer
+  }
+
+-- | The default entry TTL: 7 days (in seconds).
+defaultRedisTTL :: Integer
+defaultRedisTTL = 7 * 24 * 60 * 60
+
+-- | Connect to Redis (verifying the connection eagerly via @checkedConnect@) with
+-- the 'defaultRedisTTL'. Throws 'R.ConnectError' if the server is unreachable.
+openRedisCache :: R.ConnectInfo -> IO RedisCache
+openRedisCache = openRedisCacheWithTTL defaultRedisTTL
+
+-- | As 'openRedisCache' but with an explicit per-entry TTL (seconds).
+openRedisCacheWithTTL :: Integer -> R.ConnectInfo -> IO RedisCache
+openRedisCacheWithTTL ttl ci = do
+  conn <- R.checkedConnect ci
+  pure (RedisCache conn ttl)
+
+-- | Close the Redis connection.
+closeRedisCache :: RedisCache -> IO ()
+closeRedisCache = R.disconnect . redisConn
+
+-- | The operational Redis key for a content-addressed 'CacheKey'.
+redisKey :: CacheKey -> ByteString
+redisKey k = TE.encodeUtf8 ("shikumi:cache:" <> unCacheKey k)
+
+-- | Discharge the 'Cache' effect against Redis. A missing key, a Redis-level
+-- error, or a JSON decode failure all surface as a cache MISS ('Nothing'), so a
+-- transient hiccup or a stale-schema value is simply re-fetched.
+runCacheRedis :: (IOE :> es) => RedisCache -> Eff (Cache : es) a -> Eff es a
+runCacheRedis cache = interpret $ \_ -> \case
+  LookupCache k -> liftIO $ do
+    r <- R.runRedis (redisConn cache) (R.get (redisKey k))
+    pure $ case r of
+      Right (Just bs) -> decodeStrict' bs
+      _ -> Nothing
+  StoreCache k v -> liftIO $ do
+    _ <- R.runRedis (redisConn cache) (R.setex (redisKey k) (redisTTL cache) (BL.toStrict (encode v)))
+    pure ()
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,106 @@
+-- | EP-6 Redis backend acceptance. Round-trips a @CachedResponse@ through a live
+-- Redis over the @cachedLLM@ memoizer + a counting stub provider: the first
+-- request is a MISS (provider called once, entry stored), a subsequent request
+-- is a HIT served from Redis (no further provider call).
+--
+-- The test connects to the UNIX socket named by @REDIS_SOCKET@ (set by the dev
+-- shell; started by @just services@). When the variable is unset or no server is
+-- reachable, the suite __skips cleanly__ (prints a notice and exits 0) so CI
+-- without a Redis stays green — exactly the graceful-skip the plan requires.
+module Main (main) where
+
+import Baikai (Context, Model, Options, Response, user, _Context, _Model, _Options, _Response)
+import Control.Exception (SomeException, try)
+import Control.Lens ((&), (.~))
+import Control.Monad (void)
+import Data.ByteString (ByteString)
+import Data.Generics.Labels ()
+import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
+import Data.List.NonEmpty (NonEmpty ((:|)))
+import Data.Text.Encoding qualified as TE
+import Data.Vector qualified as V
+import Database.Redis qualified as R
+import Effectful (Eff, IOE, liftIO, runEff, type (:>))
+import Effectful.Dispatch.Dynamic (interpret)
+import Shikumi.Cache (CacheKey (unCacheKey), cacheKey, cachedLLM)
+import Shikumi.Cache.Backend.Redis (RedisCache, closeRedisCache, openRedisCache, runCacheRedis)
+import Shikumi.Effect.Time (runTime)
+import Shikumi.LLM (LLM (..), complete)
+import System.Environment (lookupEnv)
+import System.Exit (exitSuccess)
+import Test.Tasty (TestTree, defaultMain, testGroup)
+import Test.Tasty.HUnit (testCase, (@?=))
+
+fixModel :: Model
+fixModel = _Model & #modelId .~ "claude-sonnet-4-6" & #provider .~ "anthropic"
+
+fixCtx :: Context
+fixCtx = _Context & #systemPrompt .~ Just "You are helpful." & #messages .~ V.singleton (user "ping")
+
+fixOpts :: Options
+fixOpts = _Options & #temperature .~ Just 0.0 & #maxTokens .~ Just 1024
+
+stubResponse :: Response
+stubResponse = _Response
+
+-- | A counting stub interpreter of EP-1's @LLM@: every completion bumps a
+-- counter and returns a fixed response.
+runCountingLLM :: (IOE :> es) => IORef Int -> Response -> Eff (LLM : es) a -> Eff es a
+runCountingLLM ref resp = interpret $ \_ -> \case
+  Complete {} -> liftIO (modifyIORef' ref (+ 1)) >> pure resp
+  Stream {} -> pure []
+
+main :: IO ()
+main = do
+  msock <- lookupEnv "REDIS_SOCKET"
+  case msock of
+    Nothing -> skip "REDIS_SOCKET is not set"
+    Just sock -> do
+      let ci = R.defaultConnectInfo {R.connectAddr = R.ConnectAddrUnixSocket sock}
+      attempt <- try (openRedisCache ci) :: IO (Either SomeException RedisCache)
+      case attempt of
+        Left _ -> skip ("no Redis reachable at socket " <> sock)
+        Right cache -> do
+          clearKey ci
+          defaultMain (tests cache)
+          closeRedisCache cache
+  where
+    skip reason = do
+      putStrLn ("[SKIP] shikumi-cache-redis: " <> reason)
+      exitSuccess
+
+-- | Delete any entry a previous run left for the fixed request, via a throwaway
+-- connection, so the MISS→HIT counter starts honest.
+clearKey :: R.ConnectInfo -> IO ()
+clearKey ci = do
+  conn <- R.checkedConnect ci
+  void $ R.runRedis conn (R.del (redisKeyFor (cacheKey fixModel fixCtx fixOpts) :| []))
+  R.disconnect conn
+
+-- | Rebuild the backend's operational key (the backend keeps it private).
+redisKeyFor :: CacheKey -> ByteString
+redisKeyFor k = TE.encodeUtf8 ("shikumi:cache:" <> unCacheKey k)
+
+tests :: RedisCache -> TestTree
+tests cache =
+  testGroup
+    "shikumi-cache-redis"
+    [ testCase "memoize: first request MISS (provider once), repeat is a Redis HIT" $ do
+        -- Two identical requests in one run: provider hit exactly once.
+        refA <- newIORef 0
+        (r1, r2) <-
+          runEff . runTime . runCacheRedis cache . runCountingLLM refA stubResponse . cachedLLM $ do
+            a <- complete fixModel fixCtx fixOpts
+            b <- complete fixModel fixCtx fixOpts
+            pure (a, b)
+        nA <- readIORef refA
+        nA @?= 1
+        r1 @?= r2
+        -- A fresh run (fresh counter) now finds the entry already in Redis → HIT.
+        refB <- newIORef 0
+        _ <-
+          runEff . runTime . runCacheRedis cache . runCountingLLM refB stubResponse . cachedLLM $
+            complete fixModel fixCtx fixOpts
+        nB <- readIORef refB
+        nB @?= 0
+    ]
