diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
 # ChangeLog
 
+## 0.3.3
+
+* RST_STREAM now contains a proper final size.
+  [#93](https://github.com/kazu-yamamoto/quic/pull/93)
+
 ## 0.3.2
 
 * Support Unreliable Datagrams extension (RFC9221)
diff --git a/Network/QUIC/Config.hs b/Network/QUIC/Config.hs
--- a/Network/QUIC/Config.hs
+++ b/Network/QUIC/Config.hs
@@ -36,6 +36,7 @@
         :: [(EncryptionLevel, CryptoData)]
         -> ([(EncryptionLevel, CryptoData)], Bool)
     , onResetStreamReceived :: Stream -> ApplicationProtocolError -> IO ()
+    , onResetStreamReceived2 :: Maybe Stream -> ApplicationProtocolError -> Int -> IO ()
     , onServerReady :: IO ()
     , onConnectionEstablished :: ConnectionInfo -> IO ()
     }
@@ -50,6 +51,7 @@
         , onTLSExtensionCreated = id
         , onTLSHandshakeCreated = (,False)
         , onResetStreamReceived = \_ _ -> return ()
+        , onResetStreamReceived2 = \_ _ _ -> return ()
         , onServerReady = return ()
         , onConnectionEstablished = \_ -> return ()
         }
diff --git a/Network/QUIC/IO.hs b/Network/QUIC/IO.hs
--- a/Network/QUIC/IO.hs
+++ b/Network/QUIC/IO.hs
@@ -217,10 +217,11 @@
     let sid = streamId s
     sclosed <- isTxStreamClosed s
     unless sclosed $ do
+        finalSize <- getTxStreamFinalSize s
         setTxStreamClosed s
         setRxStreamClosed s
         lvl <- getEncryptionLevel conn
-        let frame = ResetStream sid aerr 0
+        let frame = ResetStream sid aerr finalSize
         putOutput conn $ OutControl lvl [frame]
     delStream conn s
 
diff --git a/Network/QUIC/Receiver.hs b/Network/QUIC/Receiver.hs
--- a/Network/QUIC/Receiver.hs
+++ b/Network/QUIC/Receiver.hs
@@ -11,6 +11,7 @@
 import Network.TLS (AlertDescription (..))
 import System.Log.FastLogger
 
+import Control.Concurrent.STM
 import Network.QUIC.Config
 import Network.QUIC.Connection
 import Network.QUIC.Connector
@@ -23,7 +24,6 @@
 import Network.QUIC.Parameters
 import Network.QUIC.Qlog
 import Network.QUIC.Recovery
-import Control.Concurrent.STM
 import Network.QUIC.Stream
 import Network.QUIC.Types as QUIC
 
@@ -242,12 +242,13 @@
 processFrame conn lvl (Ack ackInfo ackDelay) = do
     when (lvl == RTT0Level) $ closeConnection conn ProtocolViolation "ACK"
     onAckReceived (connLDCC conn) lvl ackInfo $ milliToMicro ackDelay
-processFrame conn lvl (ResetStream sid aerr _finlen) = do
+processFrame conn lvl (ResetStream sid aerr finlen) = do
     when (lvl == InitialLevel || lvl == HandshakeLevel) $
         closeConnection conn ProtocolViolation "RESET_STREAM"
     when (isSendOnly conn sid) $
         closeConnection conn StreamStateError "Received in a send-only stream"
     mstrm <- findStream conn sid
+    onResetStreamReceived2 (connHooks conn) mstrm aerr finlen
     case mstrm of
         Nothing -> return ()
         Just strm -> do
@@ -265,7 +266,9 @@
         mstrm <- findStream conn sid
         case mstrm of
             Nothing -> streamNotCreatedYet conn sid "No such stream for STOP_SENDING"
-            Just _strm -> sendFrames conn lvl [ResetStream sid err 0]
+            Just strm -> do
+                finalSize <- getTxStreamFinalSize strm
+                sendFrames conn lvl [ResetStream sid err finalSize]
 processFrame _ _ (CryptoF _ "") = return ()
 processFrame conn lvl (CryptoF off cdat) = do
     when (lvl == RTT0Level) $
diff --git a/Network/QUIC/Stream.hs b/Network/QUIC/Stream.hs
--- a/Network/QUIC/Stream.hs
+++ b/Network/QUIC/Stream.hs
@@ -13,6 +13,7 @@
     waitFinTx,
 
     -- * Misc
+    getTxStreamFinalSize,
     getTxStreamOffset,
     isTxStreamClosed,
     setTxStreamClosed,
diff --git a/Network/QUIC/Stream/Misc.hs b/Network/QUIC/Stream/Misc.hs
--- a/Network/QUIC/Stream/Misc.hs
+++ b/Network/QUIC/Stream/Misc.hs
@@ -2,6 +2,7 @@
 {-# LANGUAGE RecordWildCards #-}
 
 module Network.QUIC.Stream.Misc (
+    getTxStreamFinalSize,
     getTxStreamOffset,
     isTxStreamClosed,
     setTxStreamClosed,
@@ -26,6 +27,9 @@
 import Network.QUIC.Stream.Types
 
 ----------------------------------------------------------------
+
+getTxStreamFinalSize :: Stream -> IO Offset
+getTxStreamFinalSize Stream{..} = streamOffset <$> readIORef streamStateTx
 
 getTxStreamOffset :: Stream -> Int -> IO Offset
 getTxStreamOffset Stream{..} len = atomicModifyIORef' streamStateTx get
diff --git a/Network/QUIC/Utils.hs b/Network/QUIC/Utils.hs
--- a/Network/QUIC/Utils.hs
+++ b/Network/QUIC/Utils.hs
@@ -3,7 +3,7 @@
 
 module Network.QUIC.Utils where
 
-import Control.Exception
+import qualified Control.Exception as E
 import Control.Monad (replicateM)
 import qualified Data.ByteString as BS
 import Data.ByteString.Base16
@@ -60,18 +60,18 @@
 shortpack :: String -> ShortByteString
 shortpack = Short.toShort . C8.pack
 
-ignore :: SomeException -> IO ()
+ignore :: E.SomeException -> IO ()
 ignore se
-    | isAsyncException se = throwIO se
+    | isAsyncException se = E.throwIO se
     | otherwise = return ()
 
-isAsyncException :: Exception e => e -> Bool
+isAsyncException :: E.Exception e => e -> Bool
 isAsyncException e =
-    case fromException (toException e) of
-        Just (SomeAsyncException _) -> True
+    case E.fromException (E.toException e) of
+        Just (E.SomeAsyncException _) -> True
         Nothing -> False
 
-throughAsync :: IO a -> SomeException -> IO a
-throughAsync action (SomeException e)
-    | isAsyncException e = throwIO e
+throughAsync :: IO a -> E.SomeException -> IO a
+throughAsync action (E.SomeException e)
+    | isAsyncException e = E.throwIO e
     | otherwise = action
diff --git a/Network/QUIC/Windows.hs b/Network/QUIC/Windows.hs
--- a/Network/QUIC/Windows.hs
+++ b/Network/QUIC/Windows.hs
@@ -6,16 +6,16 @@
 
 #if defined(mingw32_HOST_OS)
 import Control.Concurrent
-import qualified Control.Exception as CE
+import qualified Control.Exception as E
 import Control.Monad
 
 windowsThreadBlockHack :: IO a -> IO a
 windowsThreadBlockHack act = do
-    var <- newEmptyMVar :: IO (MVar (Either CE.SomeException a))
-    void . forkIO $ CE.try act >>= putMVar var
+    var <- newEmptyMVar :: IO (MVar (Either E.SomeException a))
+    void . forkIO $ E.try act >>= putMVar var
     res <- takeMVar var
     case res of
-        Left e -> print e >> CE.throwIO e
+        Left e -> print e >> E.throwIO e
         Right r -> return r
 #else
 windowsThreadBlockHack :: IO a -> IO a
diff --git a/quic.cabal b/quic.cabal
--- a/quic.cabal
+++ b/quic.cabal
@@ -1,6 +1,6 @@
 cabal-version:      >=1.10
 name:               quic
-version:            0.3.2
+version:            0.3.3
 license:            BSD3
 license-file:       LICENSE
 maintainer:         kazu@iij.ad.jp
@@ -151,7 +151,7 @@
         serialise,
         stm >=2.5 && <2.6,
         tls >=2.4.0 && <2.5,
-        unix-time >=0.4.12 && <0.5
+        unix-time >=0.4.12 && <0.6
 
     if os(linux)
         cc-options: -DOS_Linux
diff --git a/test/IOSpec.hs b/test/IOSpec.hs
--- a/test/IOSpec.hs
+++ b/test/IOSpec.hs
@@ -7,6 +7,7 @@
 import qualified Control.Exception as E
 import Control.Monad
 import qualified Data.ByteString as BS
+import qualified System.Timeout as Timeout
 import Test.Hspec
 
 import Network.QUIC
@@ -86,6 +87,12 @@
             withPipe (Randomly 20) $ testRecvStreamClientStopFirst cc sc waitS
         it "don't block if server stop sending first" $ do
             withPipe (Randomly 20) $ testRecvStreamServerStopFirst cc sc waitS
+        -- RFC 9000: https://www.rfc-editor.org/rfc/rfc9000.html
+        -- Section 3.5 says STOP_SENDING asks the peer to send RESET_STREAM.
+        -- Sections 4.5 and 19.4 define RESET_STREAM Final Size as the
+        -- number of bytes sent by the RESET_STREAM sender.
+        it "sends RESET_STREAM with the bytes sent as final size" $ do
+            withPipe (DropClientPacket []) $ testResetStreamFinalSize cc sc waitS
     describe "concurrency" $ do
         it "can handle multiple clients" $ do
             withPipe (Randomly 20) $ testMultiSendRecv cc sc waitS 500
@@ -104,6 +111,39 @@
 
 assertEndOfStream :: Stream -> IO ()
 assertEndOfStream strm = recvStream strm 1024 `shouldReturn` ""
+
+testResetStreamFinalSize
+    :: C.ClientConfig -> ServerConfig -> IO () -> IO ()
+testResetStreamFinalSize cc0 sc waitS = do
+    finalSizeVar <- newEmptyMVar
+    doneVar <- newEmptyMVar
+    let request = "open"
+        payload = BS.replicate 1234 0
+        hooks = (ccHooks cc0){onResetStreamReceived2 = record finalSizeVar}
+        cc = cc0{ccHooks = hooks}
+    E.bracket (forkIO $ server request payload doneVar) killThread $ \_ ->
+        client cc request payload finalSizeVar doneVar
+  where
+    aerr = ApplicationProtocolError 0
+
+    record finalSizeVar _strm _aerr finalSize = void $ tryPutMVar finalSizeVar finalSize
+
+    client cc request payload finalSizeVar doneVar = do
+        waitS
+        C.run cc $ \conn -> do
+            strm <- stream conn
+            sendStream strm request
+            consumeBytes strm (BS.length payload)
+            stopStream strm aerr
+            mres <- Timeout.timeout 5000000 $ takeMVar finalSizeVar
+            mres `shouldBe` Just (BS.length payload)
+            putMVar doneVar ()
+
+    server request payload doneVar = run sc $ \conn -> do
+        strm <- acceptStream conn
+        consumeBytes strm (BS.length request)
+        sendStream strm payload
+        takeMVar doneVar
 
 testRecvStreamClientStopFirst
     :: C.ClientConfig -> ServerConfig -> IO () -> IO ()
