diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
 # Changelog
 
+- 0.3.0 (2026-09-06)
+  * Adds a counter overflow error.
+
 - 0.2.2 (2026-05-16)
   * Features order-of-magnitude performance improvements, especially
     on ARM platforms where NEON intrinsics are available.
diff --git a/lib/Crypto/Cipher/ChaCha20.hs b/lib/Crypto/Cipher/ChaCha20.hs
--- a/lib/Crypto/Cipher/ChaCha20.hs
+++ b/lib/Crypto/Cipher/ChaCha20.hs
@@ -3,6 +3,7 @@
 {-# LANGUAGE MagicHash #-}
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE UnboxedTuples #-}
+{-# LANGUAGE ViewPatterns #-}
 
 -- |
 -- Module: Crypto.Cipher.ChaCha20
@@ -272,8 +273,9 @@
 
 -- | Error values.
 data Error =
-    InvalidKey   -- ^ the provided key was not 256 bits long
-  | InvalidNonce -- ^ the provided nonce was none 96 bits long
+    CounterOverflow -- ^ the counter overflowed
+  | InvalidKey      -- ^ the provided key was not 256 bits long
+  | InvalidNonce    -- ^ the provided nonce was not 96 bits long
   deriving (Eq, Show)
 
 -- RFC8439 2.3
@@ -332,26 +334,33 @@
 --
 --   >>> let key = "don't tell anyone my secret key!"
 --   >>> let non = "or my nonce!"
---   >>> let cip = cipher key 1 non "but you can share the plaintext"
+--   >>> let Right cip = cipher key 1 non "but you can share the ciphertext"
 --   >>> cip
---   "\192*c\248A\204\211n\130y8\197\146k\245\178Y\197=\180_\223\138\146:^\206\&0\v[\201"
+--   "\192*c\248A\204\211n\130y8\197\146k\245\178Y\197=\180_\223\153\151+_\197\&6\SUBF\197\160"
 --   >>> cipher key 1 non cip
---   Right "but you can share the plaintext"
+--   Right "but you can share the ciphertext"
 cipher
   :: BS.ByteString    -- ^ 256-bit key
   -> Word32           -- ^ 32-bit counter
   -> BS.ByteString    -- ^ 96-bit nonce
   -> BS.ByteString    -- ^ arbitrary-length plaintext
   -> Either Error BS.ByteString    -- ^ ciphertext
-cipher raw_key@(BI.PS _ _ kl) counter raw_nonce@(BI.PS _ _ nl) plaintext
-  | kl /= 32 = Left InvalidKey
-  | nl /= 12 = Left InvalidNonce
-  | Arm.chacha20_arm_available =
-      Right (Arm.cipher raw_key counter raw_nonce plaintext)
-  | otherwise = pure $ runST $ do
-      let key = _parse_key raw_key
-          non = _parse_nonce raw_nonce
-      _cipher key counter non plaintext
+cipher
+        raw_key@(BI.PS _ _ kl)
+        counter
+        raw_nonce@(BI.PS _ _ nl)
+        plaintext@(BI.PS _ _ (fi -> pl))
+    | kl /= 32  = Left InvalidKey
+    | nl /= 12  = Left InvalidNonce
+    | pl > room = Left CounterOverflow
+    | Arm.chacha20_arm_available =
+        Right (Arm.cipher raw_key counter raw_nonce plaintext)
+    | otherwise = pure $ runST $ do
+        let key = _parse_key raw_key
+            non = _parse_nonce raw_nonce
+        _cipher key counter non plaintext
+  where
+    room = (0x100000000 - fi counter) * 64 :: Word64
 
 _cipher
   :: PrimMonad m
diff --git a/ppad-chacha.cabal b/ppad-chacha.cabal
--- a/ppad-chacha.cabal
+++ b/ppad-chacha.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-chacha
-version:            0.2.2
+version:            0.3.0
 synopsis:           A fast ChaCha20 stream cipher
 license:            MIT
 license-file:       LICENSE
@@ -36,6 +36,7 @@
     ghc-options: -fllvm -O2
   exposed-modules:
       Crypto.Cipher.ChaCha20
+  other-modules:
       Crypto.Cipher.ChaCha20.Arm
   build-depends:
       base >= 4.9 && < 5
@@ -103,4 +104,3 @@
     , deepseq
     , ppad-chacha
     , weigh
-
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -26,6 +26,8 @@
   , crypt1
   , crypt2
   , crypt3
+  , counter_boundary
+  , counter_overflow
   ]
 
 quarter :: TestTree
@@ -162,3 +164,27 @@
       Right out = ChaCha.cipher key con non plain
   H.assertEqual mempty cip out
 
+-- counter overflow
+
+overflow_key :: BS.ByteString
+overflow_key = fromJust . B16.decode $
+  "0000000000000000000000000000000000000000000000000000000000000000"
+
+overflow_non :: BS.ByteString
+overflow_non = fromJust . B16.decode $ "000000000000000000000000"
+
+counter_boundary :: TestTree
+counter_boundary = H.testCase "chacha20 encrypt (counter boundary)" $ do
+  let con = maxBound - 1
+      plain = BS.replicate 128 0
+      Right b0 = ChaCha.block overflow_key con overflow_non
+      Right b1 = ChaCha.block overflow_key maxBound overflow_non
+      Right out = ChaCha.cipher overflow_key con overflow_non plain
+  H.assertEqual mempty (b0 <> b1) out
+
+counter_overflow :: TestTree
+counter_overflow = H.testCase "chacha20 encrypt (counter overflow)" $ do
+  let con = maxBound - 1
+      plain = BS.replicate 129 0
+      out = ChaCha.cipher overflow_key con overflow_non plain
+  H.assertEqual mempty (Left ChaCha.CounterOverflow) out
