diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,8 @@
-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.10.5.0...main)
+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.10.5.1...main)
+
+## [v1.10.5.1](https://github.com/freckle/freckle-app/compare/v1.10.5.0...v1.10.5.1)
+
+- Fix decoding bug when caching non-UTF-8 values
 
 ## [v1.10.5.0](https://github.com/freckle/freckle-app/compare/v1.10.4.0...v1.10.5.0)
 
diff --git a/freckle-app.cabal b/freckle-app.cabal
--- a/freckle-app.cabal
+++ b/freckle-app.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.18
 name:               freckle-app
-version:            1.10.5.0
+version:            1.10.5.1
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
diff --git a/library/Freckle/App/Memcached/Client.hs b/library/Freckle/App/Memcached/Client.hs
--- a/library/Freckle/App/Memcached/Client.hs
+++ b/library/Freckle/App/Memcached/Client.hs
@@ -90,7 +90,7 @@
         { Trace.attributes =
             HashMap.fromList
               [ ("key", Trace.toAttribute k)
-              , ("value", Trace.toAttribute $ decodeUtf8 v)
+              , ("value", byteStringToAttribute v)
               , ("expiration", Trace.toAttribute expiration)
               ]
         }
diff --git a/library/Freckle/App/OpenTelemetry.hs b/library/Freckle/App/OpenTelemetry.hs
--- a/library/Freckle/App/OpenTelemetry.hs
+++ b/library/Freckle/App/OpenTelemetry.hs
@@ -49,12 +49,20 @@
     -- ** 'Tracer'
   , makeTracer
   , tracerOptions
+
+    -- ** Utilities
+  , byteStringToAttribute
+  , attributeValueLimit
   ) where
 
 import Freckle.App.Prelude
 
 import Blammo.Logging (withThreadContext, (.=))
 import Control.Monad.Catch (MonadMask)
+import Data.ByteString (ByteString)
+import qualified Data.Text as T
+import Data.Text.Encoding (decodeUtf8With)
+import Data.Text.Encoding.Error (lenientDecode)
 import Data.Word (Word64)
 import OpenTelemetry.Context (lookupSpan)
 import OpenTelemetry.Context.ThreadLocal (getContext)
@@ -123,3 +131,29 @@
 withTraceIdContext f = do
   mTraceId <- getCurrentTraceIdAsDatadog
   maybe f (\traceId -> withThreadContext ["trace_id" .= traceId] f) mTraceId
+
+-- | Convert a 'ByteString' to an 'Attribute' safely
+--
+-- - Decodes it as UTF-8 leniently,
+-- - Truncates to fit within 'attributeValueLimit'
+byteStringToAttribute :: ByteString -> Attribute
+byteStringToAttribute =
+  toAttribute
+    . truncateText attributeValueLimit
+    . decodeUtf8With lenientDecode
+
+-- | Character limit for 'Attribute' values
+--
+-- OTel the spec doesn't specify a limit, but says that SDKs should decide
+-- some limit. It's not clear what the Haskell SDK does, if anything. New
+-- Relic applies a limit of 4095 characters on all metrics it handles,
+-- including those coming from OTel. Seems reasonable enough.
+--
+-- <https://docs.newrelic.com/docs/more-integrations/open-source-telemetry-integrations/opentelemetry/best-practices/opentelemetry-best-practices-attributes/>
+attributeValueLimit :: Int
+attributeValueLimit = 4095
+
+truncateText :: Int -> Text -> Text
+truncateText l t
+  | T.length t <= l = t
+  | otherwise = T.take (l - 3) t <> "..."
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
 name: freckle-app
-version: 1.10.5.0
+version: 1.10.5.1
 maintainer: Freckle Education
 category: Utils
 github: freckle/freckle-app
