diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
 ## Insertion
 
 For the sake of reliability, insertion is performed locally.
+Insertions can be batched up to one commit in order to reduce overheads.
 
 ```haskell
 commitFile :: FilePath -> Transaction a -> IO a
@@ -29,3 +30,55 @@
 $ liszt foo.liszt message -b 0 -f "%p\n"
 hello, world
 ```
+
+# Representation
+
+Liszt employs a 2-3 tree (special case of B tree) of skew binary random access lists as its on-disk format.
+
+A liszt file consists of six types of contents: keys, payloads, tags, nodes, trees, and a footer.
+
+`Key`s, `Tag`s, and `Payload`s are bytestrings with arbitrary length.
+
+A pointer to a content with type `t` is denoted by `P t`.
+It's a pair of the byte offset and the length of the content, encoded in unsigned VLQ.
+
+A node is one of the following:
+
+* Empty node: `0x00`
+* 1-leaf: `0x01 (P Key) Spine`
+* 2-leaf: `0x02 (P Key) Spine (P Key) Spine`
+* 2-node: `0x12 (P Node) (P Key) Spine (P Node)`
+* 3-node: `0x13 (P Node) (P Key) Spine (P Node) (P Key) Spine (P Node)`
+
+Spine is a list of pairs of:
+
+* Rank: the number of payloads in a tree
+* Pointer to a tree
+
+Spine prefixed by the length of the list.
+
+* `Spine`: `Int [Int (P Tree)]`
+
+Tree is either
+
+* Tip: `0x80 Int Tag (P Payload)`
+* Bin: `0x81 Int Tag (P Payload) (P Node) (P Node)`
+
+where the Int represents the length of the `Tag`.
+
+A footer is a root node padded to 256 bytes long, and the padding must end with the following byte sequence:
+
+```
+8df4 c865 fa3d 300a f3f8 9962 e049 e379 489c e4cd 2b52 a630 5584 004c 4953 5a54
+```
+
+(last 5 bytes is LISZT)
+
+Committing is performed in the following steps. This append-only scheme grants robustness to some extent.
+
+* Read the root node from the file.
+* Append payloads.
+* Append a new footer.
+
+When an exception occurs in the middle, it will put the original footer to prevent corruption
+(TODO: find the last footer in case of corrupted file).
diff --git a/app/server.hs b/app/server.hs
--- a/app/server.hs
+++ b/app/server.hs
@@ -28,4 +28,3 @@
   (_, _, es) -> do
     name <- getProgName
     die $ unlines es ++ usageInfo name options
-
diff --git a/benchmarks/bench.hs b/benchmarks/bench.hs
new file mode 100644
--- /dev/null
+++ b/benchmarks/bench.hs
@@ -0,0 +1,9 @@
+{-# LANGUAGE OverloadedStrings #-}
+import Gauge.Main
+import Control.Monad
+import Database.Liszt
+
+main = withLiszt "bench.liszt" $ \h -> do
+    defaultMain
+        [ bench "insert" $ nfIO $ commit h $ do
+          forM_ [1..10] $ \i -> insert "test" (i :: Int) ]
diff --git a/liszt.cabal b/liszt.cabal
--- a/liszt.cabal
+++ b/liszt.cabal
@@ -1,11 +1,11 @@
--- This file has been generated from package.yaml by hpack version 0.20.0.
+-- This file has been generated from package.yaml by hpack version 0.28.2.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 5561a69ed2e98f7b84def2db25e066ef420542febb624a57bdbe15bb4c7c521a
+-- hash: 7cbc3b7d828fd5acf9d0c70234dd19f9cbd19c7e12d3f47b7d96f40b339fc2fd
 
 name:           liszt
-version:        0.1
+version:        0.2
 synopsis:       Append only key-list database
 description:    Please see the README on GitHub at <https://github.com/fumieval/liszt#readme>
 category:       Database
@@ -18,7 +18,6 @@
 license-file:   LICENSE
 build-type:     Simple
 cabal-version:  >= 1.10
-
 extra-source-files:
     ChangeLog.md
     README.md
@@ -28,13 +27,23 @@
   location: https://github.com/fumieval/liszt
 
 library
+  exposed-modules:
+      Database.Liszt
+      Database.Liszt.Tracker
+      Database.Liszt.Internal
+      Database.Liszt.Network
+  other-modules:
+      Database.Liszt.Internal.Decoder
+      Paths_liszt
   hs-source-dirs:
       src
   build-depends:
       base >=4.7 && <5
     , binary
     , bytestring
+    , cereal
     , containers
+    , cpu
     , deepseq
     , directory
     , exceptions
@@ -49,25 +58,24 @@
     , text
     , transformers
     , unordered-containers
+    , vector
+    , vector-th-unbox
     , winery
-  exposed-modules:
-      Database.Liszt
-      Database.Liszt.Tracker
-      Database.Liszt.Internal
-      Database.Liszt.Network
-  other-modules:
-      Paths_liszt
   default-language: Haskell2010
 
 executable liszt
   main-is: client.hs
+  other-modules:
+      Paths_liszt
   hs-source-dirs:
       app
   build-depends:
       base >=4.7 && <5
     , binary
     , bytestring
+    , cereal
     , containers
+    , cpu
     , deepseq
     , directory
     , exceptions
@@ -83,21 +91,25 @@
     , text
     , transformers
     , unordered-containers
+    , vector
+    , vector-th-unbox
     , winery
-  other-modules:
-      Paths_liszt
   default-language: Haskell2010
 
 executable lisztd
   main-is: server.hs
+  other-modules:
+      Paths_liszt
   hs-source-dirs:
       app
-  ghc-options: -threaded
+  ghc-options: -threaded -O2
   build-depends:
       base >=4.7 && <5
     , binary
     , bytestring
+    , cereal
     , containers
+    , cpu
     , deepseq
     , directory
     , exceptions
@@ -113,7 +125,43 @@
     , text
     , transformers
     , unordered-containers
+    , vector
+    , vector-th-unbox
     , winery
+  default-language: Haskell2010
+
+benchmark bench
+  type: exitcode-stdio-1.0
+  main-is: bench.hs
   other-modules:
       Paths_liszt
+  hs-source-dirs:
+      benchmarks
+  ghc-options: -O2
+  build-depends:
+      base >=4.7 && <5
+    , binary
+    , bytestring
+    , cereal
+    , containers
+    , cpu
+    , deepseq
+    , directory
+    , exceptions
+    , filepath
+    , fsnotify
+    , gauge
+    , liszt
+    , network
+    , reflection
+    , scientific
+    , sendfile
+    , stm
+    , stm-delay
+    , text
+    , transformers
+    , unordered-containers
+    , vector
+    , vector-th-unbox
+    , winery
   default-language: Haskell2010
diff --git a/src/Database/Liszt.hs b/src/Database/Liszt.hs
--- a/src/Database/Liszt.hs
+++ b/src/Database/Liszt.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE LambdaCase #-}
 module Database.Liszt (
     openLiszt,
     closeLiszt,
@@ -13,23 +14,30 @@
     insertRaw,
     commit,
     commitFile,
-    -- * Reader
+    -- * Local reader
+    RawPointer,
+    count,
+    fetchRange,
+    fetchPayload,
+    -- * Remote reader
     Offset(..),
     Request(..),
     defRequest,
     Connection,
     withConnection,
-    fetch
+    fetch,
+    LisztError(..)
     ) where
 
+import Control.Monad.IO.Class
 import Database.Liszt.Internal
 import Database.Liszt.Network
 import Database.Liszt.Tracker
 import Data.Winery
 
 -- | Commit a 'Transaction' to a file.
-commitFile :: FilePath -> Transaction a -> IO a
-commitFile path m = withLiszt path $ \h -> commit h m
+commitFile :: (MonadIO m) => FilePath -> Transaction a -> m a
+commitFile path m = liftIO $ withLiszt path $ \h -> commit h m
 
 -- | Insert a value.
 insert :: Serialise a => Key -> a -> Transaction ()
@@ -42,3 +50,25 @@
 insertTagged :: (Serialise t, Serialise a) => Key -> t -> a -> Transaction ()
 insertTagged k t v = insertRaw k (toEncoding t) (toEncoding v)
 {-# INLINE insertTagged #-}
+
+-- | The number of entries in the stream
+count :: MonadIO m => LisztHandle -> Key -> m Int
+count h k = liftIO $ do
+  root <- fetchRoot h
+  maybe 0 spineLength <$> lookupSpine h k root
+
+fetchRange :: MonadIO m => LisztHandle -> Key -> Int -> Int -> m [(Int, Tag, RawPointer)]
+fetchRange h key i_ j_ = liftIO $ do
+  root <- fetchRoot h
+  lookupSpine h key root >>= \case
+    Nothing -> return []
+    Just spine -> do
+      let len = spineLength spine
+      let normalise x
+            | x < 0 = min (len - 1) $ max 0 $ len + x
+            | otherwise = min (len - 1) x
+      let j = normalise j_
+      let i = normalise i_
+      spine' <- dropSpine h (len - j - 1) spine
+      result <- takeSpine h (j - i + 1) spine' []
+      return [(k, t, rp) | (k, (t, rp)) <- zip [i..] result]
diff --git a/src/Database/Liszt/Internal.hs b/src/Database/Liszt/Internal.hs
--- a/src/Database/Liszt/Internal.hs
+++ b/src/Database/Liszt/Internal.hs
@@ -17,13 +17,14 @@
   , TransactionState
   -- * Reading
   , availableKeys
-  -- * Frame
-  , Frame(..)
-  , decodeFrame
-  , forceSpine
+  -- * Node
+  , Node(..)
+  , peekNode
+  , LisztDecodingException(..)
   -- * Fetching
   , Fetchable(..)
   , RawPointer(..)
+  , fetchPayload
   -- * Footer
   , fetchRoot
   , footerSize
@@ -33,6 +34,7 @@
   , Spine
   , spineLength
   , QueryResult
+  , wholeSpine
   , takeSpine
   , dropSpine
   , takeSpineWhile
@@ -45,14 +47,15 @@
 import Control.Monad.Catch
 import Control.Monad.IO.Class
 import Control.Monad.Trans.State.Strict
+import Data.Bifunctor
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Internal as B
 import qualified Data.IntMap.Strict as IM
 import Data.IORef
-import Data.Proxy
+import Data.Monoid
 import Data.Word
-import Data.Winery
-import qualified Data.Winery.Internal.Builder as WB
+import Data.Winery.Internal.Builder
+import Database.Liszt.Internal.Decoder
 import Foreign.ForeignPtr
 import Foreign.Ptr
 import GHC.Generics (Generic)
@@ -63,32 +66,81 @@
 
 -- | Tag is an extra value attached to a payload. This can be used to perform
 -- a binary search.
-type Tag = WB.Encoding
+type Tag = B.ByteString
 
 type Spine a = [(Int, a)]
 
-newtype KeyPointer = KeyPointer RawPointer deriving (Show, Eq, Serialise, NFData)
+newtype KeyPointer = KeyPointer RawPointer deriving (Show, Eq, NFData)
 
 data RawPointer = RP !Int !Int deriving (Show, Eq, Generic)
-instance Serialise RawPointer
 instance NFData RawPointer where
   rnf r = r `seq` ()
 
-data Frame a = Empty
+data Node t a = Empty
   | Leaf1 !KeyPointer !(Spine a)
   | Leaf2 !KeyPointer !(Spine a) !KeyPointer !(Spine a)
   | Node2 !a !KeyPointer !(Spine a) !a
   | Node3 !a !KeyPointer !(Spine a) !a !KeyPointer !(Spine a) !a
-  | Tree !Tag !RawPointer !a !a
-  | Leaf !Tag !RawPointer
+  | Tip !t !RawPointer
+  | Bin !t !RawPointer !a !a
   deriving (Generic, Show, Eq, Functor, Foldable, Traversable)
 
-instance Serialise a => Serialise (Frame a)
+encodeNode :: Node Encoding RawPointer -> Encoding
+encodeNode Empty = word8 0x00
+encodeNode (Leaf1 (KeyPointer p) s) = word8 0x01 <> encodeRP p <> encodeSpine s
+encodeNode (Leaf2 (KeyPointer p) s (KeyPointer q) t) = word8 0x02
+  <> encodeRP p <> encodeSpine s <> encodeRP q <> encodeSpine t
+encodeNode (Node2 l (KeyPointer p) s r) = word8 0x12
+  <> encodeRP l <> encodeRP p <> encodeSpine s <> encodeRP r
+encodeNode (Node3 l (KeyPointer p) s m (KeyPointer q) t r) = word8 0x13
+  <> encodeRP l <> encodeRP p <> encodeSpine s
+  <> encodeRP m <> encodeRP q <> encodeSpine t <> encodeRP r
+encodeNode (Tip t p) = word8 0x80 <> unsignedVarInt (getSize t) <> t <> encodeRP p
+encodeNode (Bin t p l r) = word8 0x81 <> unsignedVarInt (getSize t) <> t
+  <> encodeRP p <> encodeRP l <> encodeRP r
 
-decodeFrame :: B.ByteString -> Frame RawPointer
-decodeFrame = either (error . show) id $ getDecoder
-  $ schema (Proxy :: Proxy (Frame RawPointer))
+encodeRP :: RawPointer -> Encoding
+encodeRP (RP p l) = unsignedVarInt p <> unsignedVarInt l
 
+encodeSpine :: Spine RawPointer -> Encoding
+encodeSpine s = unsignedVarInt (length s) <> foldMap (\(r, p) -> unsignedVarInt r <> encodeRP p) s
+
+data LisztDecodingException = LisztDecodingException deriving Show
+instance Exception LisztDecodingException
+
+peekNode :: Ptr Word8 -> IO (Node Tag RawPointer)
+peekNode = runDecoder $ decodeWord8 >>= \case
+  0x00 -> return Empty
+  0x01 -> Leaf1 <$> kp <*> spine
+  0x02 -> Leaf2 <$> kp <*> spine <*> kp <*> spine
+  0x12 -> Node2 <$> rp <*> kp <*> spine <*> rp
+  0x13 -> Node3 <$> rp <*> kp <*> spine <*> rp <*> kp <*> spine <*> rp
+  0x80 -> Tip <$> bs <*> rp
+  0x81 -> Bin <$> bs <*> rp <*> rp <*> rp
+  _ -> throwM LisztDecodingException
+  where
+    kp = fmap KeyPointer rp
+    rp = RP <$> decodeVarInt <*> decodeVarInt
+    spine = do
+      len <- decodeVarInt
+      replicateM len $ (,) <$> decodeVarInt <*> rp
+    bs = do
+      len <- decodeVarInt
+      Decoder $ \src -> do
+        fp <- B.mallocByteString len
+        withForeignPtr fp $ \dst -> B.memcpy dst src len
+        return $ DecodeResult (src `plusPtr` len) (B.PS fp 0 len)
+
+instance Bifunctor Node where
+  bimap f g = \case
+    Empty -> Empty
+    Leaf1 k s -> Leaf1 k (map (fmap g) s)
+    Leaf2 j s k t -> Leaf2 j (map (fmap g) s) k (map (fmap g) t)
+    Node2 l k s r -> Node2 (g l) k (map (fmap g) s) (g r)
+    Node3 l j s m k t r -> Node3 (g l) j (map (fmap g) s) (g m) k (map (fmap g) t) (g r)
+    Bin t p l r -> Bin (f t) p (g l) (g r)
+    Tip t p -> Tip (f t) p
+
 data LisztHandle = LisztHandle
   { hPayload :: !Handle
   , refBuffer :: MVar (Int, ForeignPtr Word8)
@@ -123,8 +175,8 @@
 data TransactionState = TS
   { dbHandle :: !LisztHandle
   , freshId :: !Int
-  , pending :: !(IM.IntMap (Frame StagePointer))
-  , currentRoot :: !(Frame StagePointer)
+  , pending :: !(IM.IntMap (Node Encoding StagePointer))
+  , currentRoot :: !(Node Encoding StagePointer)
   , currentPos :: !Int
   }
 
@@ -144,19 +196,19 @@
   writeIORef refModified True
   cont hPayload
 
-insertRaw :: Key -> Tag -> Encoding -> Transaction ()
+insertRaw :: Key -> Encoding -> Encoding -> Transaction ()
 insertRaw key tag payload = do
   lh <- gets dbHandle
   ofs <- gets currentPos
   liftIO $ append lh $ \h -> hPutEncoding h payload
   root <- gets currentRoot
-  modify $ \ts -> ts { currentPos = ofs + WB.getSize payload }
-  insertF key (insertSpine tag (ofs `RP` WB.getSize payload)) root >>= \case
+  modify $ \ts -> ts { currentPos = ofs + getSize payload }
+  insertF key (insertSpine tag (ofs `RP` getSize payload)) root >>= \case
     Pure t -> modify $ \ts -> ts { currentRoot = t }
     Carry l k a r -> modify $ \ts -> ts { currentRoot = Node2 l k a r }
 
-addFrame :: Frame StagePointer -> Transaction StagePointer
-addFrame f = state $ \ts -> (Uncommited (freshId ts), ts
+addNode :: Node Encoding StagePointer -> Transaction StagePointer
+addNode f = state $ \ts -> (Uncommited (freshId ts), ts
   { freshId = freshId ts + 1
   , pending = IM.insert (freshId ts) f (pending ts) })
 
@@ -176,7 +228,7 @@
   root <- fetchRoot h
   do
     (a, TS _ _ pendings root' offset1) <- runStateT transaction
-      $ TS h 0 IM.empty (fmap Commited root) offset0
+      $ TS h 0 IM.empty (bimap bytes Commited root) offset0
 
     let substP (Commited ofs) = return ofs
         substP (Uncommited i) = case IM.lookup i pendings of
@@ -202,15 +254,15 @@
           qv' <- traverse (traverse substP) qv
           r' <- substP r
           write (Node3 l' pk pv' m' qk qv' r')
-        substF (Tree t p l r) = do
+        substF (Bin t p l r) = do
           l' <- substP l
           r' <- substP r
-          write (Tree t p l' r')
-        substF (Leaf t p) = write (Leaf t p)
+          write (Bin t p l' r')
+        substF (Tip t p) = write (Tip t p)
 
     writeFooter offset1 $ substF root'
     return ((), a)
-    `onException` writeFooter offset0 (write root)
+    `onException` writeFooter offset0 (write $ first bytes root)
   where
     writeFooter ofs m = do
       modified <- readIORef (refModified h)
@@ -222,26 +274,26 @@
         writeIORef (refModified h) False
 
     h' = hPayload h
-    write :: Frame RawPointer -> StateT Int IO RawPointer
+    write :: Node Encoding RawPointer -> StateT Int IO RawPointer
     write f = do
       ofs <- get
-      let e = toEncoding f
+      let e = encodeNode f
       liftIO $ hPutEncoding h' e
-      put $! ofs + WB.getSize e
-      return $ RP ofs (WB.getSize e)
+      put $! ofs + getSize e
+      return $ RP ofs (getSize e)
 
 fetchKeyT :: KeyPointer -> Transaction Key
 fetchKeyT p = gets dbHandle >>= \h -> liftIO (fetchKey h p)
 
-fetchStage :: StagePointer -> Transaction (Frame StagePointer)
+fetchStage :: StagePointer -> Transaction (Node Encoding StagePointer)
 fetchStage (Commited p) = do
   h <- gets dbHandle
-  liftIO $ fmap Commited <$> fetchFrame h p
+  liftIO $ bimap bytes Commited <$> fetchNode h p
 fetchStage (Uncommited i) = gets pending >>= return . maybe (error "fetch: not found") id . IM.lookup i
 
 insertF :: Key
   -> (Spine StagePointer -> Transaction (Spine StagePointer))
-  -> Frame StagePointer
+  -> Node Encoding StagePointer
   -> Transaction (Result StagePointer)
 insertF k u Empty = Pure <$> (Leaf1 <$> allocKey k <*> u [])
 insertF k u (Leaf1 pk pv) = do
@@ -260,8 +312,8 @@
     LT -> do
       v <- u []
       kp <- allocKey k
-      l <- addFrame $ Leaf1 kp v
-      r <- addFrame $ Leaf1 qk qv
+      l <- addNode $ Leaf1 kp v
+      r <- addNode $ Leaf1 qk qv
       return $ Carry l pk pv r
     EQ -> do
       v <- u pv
@@ -271,8 +323,8 @@
       case compare k vqk of
         LT -> do
           v <- u []
-          l <- addFrame $ Leaf1 pk pv
-          r <- addFrame $ Leaf1 qk qv
+          l <- addNode $ Leaf1 pk pv
+          r <- addNode $ Leaf1 qk qv
           kp <- allocKey k
           return $ Carry l kp v r
         EQ -> do
@@ -281,8 +333,8 @@
         GT -> do
           v <- u []
           kp <- allocKey k
-          l <- addFrame $ Leaf1 pk pv
-          r <- addFrame $ Leaf1 kp v
+          l <- addNode $ Leaf1 pk pv
+          r <- addNode $ Leaf1 kp v
           return $ Carry l qk qv r
 insertF k u (Node2 l pk0 pv0 r) = do
   vpk0 <- fetchKeyT pk0
@@ -291,7 +343,7 @@
       fl <- fetchStage l
       insertF k u fl >>= \case
         Pure l' -> do
-          l'' <- addFrame l'
+          l'' <- addNode l'
           return $ Pure $ Node2 l'' pk0 pv0 r
         Carry l' ck cv r' -> return $ Pure $ Node3 l' ck cv r' pk0 pv0 r
     EQ -> do
@@ -301,7 +353,7 @@
       fr <- fetchStage r
       insertF k u fr >>= \case
         Pure r' -> do
-          r'' <- addFrame r'
+          r'' <- addNode r'
           return $ Pure $ Node2 l pk0 pv0 r''
         Carry l' ck cv r' -> return $ Pure $ Node3 l pk0 pv0 l' ck cv r'
 insertF k u (Node3 l pk0 pv0 m qk0 qv0 r) = do
@@ -311,11 +363,11 @@
       fl <- fetchStage l
       insertF k u fl >>= \case
         Pure l' -> do
-          l'' <- addFrame l'
+          l'' <- addNode l'
           return $ Pure $ Node3 l'' pk0 pv0 m qk0 qv0 r
         Carry l' ck cv r' -> do
-          bl <- addFrame (Node2 l' ck cv r')
-          br <- addFrame (Node2 m qk0 qv0 r)
+          bl <- addNode (Node2 l' ck cv r')
+          br <- addNode (Node2 m qk0 qv0 r)
           return $ Pure $ Node2 bl pk0 pv0 br
     EQ -> do
       v <- u pv0
@@ -327,11 +379,11 @@
           fm <- fetchStage m
           insertF k u fm >>= \case
             Pure m' -> do
-              m'' <- addFrame m'
+              m'' <- addNode m'
               return $ Pure $ Node3 l pk0 pv0 m'' qk0 qv0 r
             Carry l' ck cv r' -> do
-              bl <- addFrame $ Node2 l pk0 pv0 l'
-              br <- addFrame $ Node2 r' qk0 qv0 r
+              bl <- addNode $ Node2 l pk0 pv0 l'
+              br <- addNode $ Node2 r' qk0 qv0 r
               return $ Pure $ Node2 bl ck cv br
         EQ -> do
           v <- u qv0
@@ -340,34 +392,34 @@
           fr <- fetchStage r
           insertF k u fr >>= \case
             Pure r' -> do
-              r'' <- addFrame r'
+              r'' <- addNode r'
               return $ Pure $ Node3 l pk0 pv0 m qk0 qv0 r''
             Carry l' ck cv r' -> do
-              bl <- addFrame $ Node2 l pk0 pv0 m
-              br <- addFrame $ Node2 l' ck cv r'
+              bl <- addNode $ Node2 l pk0 pv0 m
+              br <- addNode $ Node2 l' ck cv r'
               return $ Pure $ Node2 bl qk0 qv0 br
-insertF _ _ (Tree _ _ _ _) = fail "Unexpected Tree"
-insertF _ _ (Leaf _ _) = fail "Unexpected Leaf"
+insertF _ _ (Bin _ _ _ _) = fail "Unexpected Tree"
+insertF _ _ (Tip _ _) = fail "Unexpected Leaf"
 
-data Result a = Pure (Frame a)
+data Result a = Pure (Node Encoding a)
   | Carry !a !KeyPointer !(Spine a) !a
 
-insertSpine :: Tag -> RawPointer -> Spine StagePointer -> Transaction (Spine StagePointer)
+insertSpine :: Encoding -> RawPointer -> Spine StagePointer -> Transaction (Spine StagePointer)
 insertSpine tag p ((m, x) : (n, y) : ss) | m == n = do
-  t <- addFrame $ Tree tag p x y
+  t <- addNode $ Bin tag p x y
   return $ (2 * m + 1, t) : ss
 insertSpine tag p ss = do
-  t <- addFrame $ Leaf tag p
+  t <- addNode $ Tip tag p
   return $ (1, t) : ss
 
 --------------------------------------------------------------------------------
 -- Fetching
 
 class Fetchable a where
-  fetchFrame :: LisztHandle -> a -> IO (Frame a)
+  fetchNode :: LisztHandle -> a -> IO (Node Tag a)
 
 instance Fetchable RawPointer where
-  fetchFrame h (RP ofs len) = fetchFrame'
+  fetchNode h (RP ofs len) = fetchNode'
     (hSeek (hPayload h) AbsoluteSeek (fromIntegral ofs)) h len
 
 fetchKey :: LisztHandle -> KeyPointer -> IO Key
@@ -381,8 +433,8 @@
       modifyIORef' keyCache (IM.insert ofs key)
       return key
 
-fetchFrame' :: IO () -> LisztHandle -> Int -> IO (Frame RawPointer)
-fetchFrame' seek h len = modifyMVar (refBuffer h) $ \(blen, buf) -> do
+fetchNode' :: IO () -> LisztHandle -> Int -> IO (Node Tag RawPointer)
+fetchNode' seek h len = modifyMVar (refBuffer h) $ \(blen, buf) -> do
   seek
   (blen', buf') <- if blen < len
     then do
@@ -391,11 +443,11 @@
     else return (blen, buf)
   f <- withForeignPtr buf' $ \ptr -> do
     _ <- hGetBuf (hPayload h) ptr len
-    let f = decodeFrame $ B.PS buf' 0 len
-    forceSpine f
+    peekNode ptr
   return ((blen', buf'), f)
+{-# INLINE fetchNode' #-}
 
-lookupSpine :: Fetchable a => LisztHandle -> Key -> Frame a -> IO (Maybe (Spine a))
+lookupSpine :: Fetchable a => LisztHandle -> Key -> Node tag a -> IO (Maybe (Spine a))
 lookupSpine h k (Leaf1 p v) = do
   vp <- fetchKey h p
   if k == vp then return (Just v) else return Nothing
@@ -407,40 +459,45 @@
 lookupSpine h k (Node2 l p v r) = do
   vp <- fetchKey h p
   case compare k vp of
-    LT -> fetchFrame h l >>= lookupSpine h k
+    LT -> fetchNode h l >>= lookupSpine h k
     EQ -> return (Just v)
-    GT -> fetchFrame h r >>= lookupSpine h k
+    GT -> fetchNode h r >>= lookupSpine h k
 lookupSpine h k (Node3 l p u m q v r) = do
   vp <- fetchKey h p
   case compare k vp of
-    LT -> fetchFrame h l >>= lookupSpine h k
+    LT -> fetchNode h l >>= lookupSpine h k
     EQ -> return (Just u)
     GT -> do
       vq <- fetchKey h q
       case compare k vq of
-        LT -> fetchFrame h m >>= lookupSpine h k
+        LT -> fetchNode h m >>= lookupSpine h k
         EQ -> return (Just v)
-        GT -> fetchFrame h r >>= lookupSpine h k
+        GT -> fetchNode h r >>= lookupSpine h k
 lookupSpine _ _ _ = return Nothing
 
-availableKeys :: LisztHandle -> Frame RawPointer -> IO [Key]
+availableKeys :: LisztHandle -> Node t RawPointer -> IO [Key]
 availableKeys _ Empty = return []
 availableKeys h (Leaf1 k _) = pure <$> fetchKey h k
 availableKeys h (Leaf2 j _ k _) = sequence [fetchKey h j, fetchKey h k]
 availableKeys h (Node2 l k _ r) = do
-  lks <- fetchFrame h l >>= availableKeys h
+  lks <- fetchNode h l >>= availableKeys h
   vk <- fetchKey h k
-  rks <- fetchFrame h r >>= availableKeys h
+  rks <- fetchNode h r >>= availableKeys h
   return $ lks ++ vk : rks
 availableKeys h (Node3 l j _ m k _ r) = do
-  lks <- fetchFrame h l >>= availableKeys h
+  lks <- fetchNode h l >>= availableKeys h
   vj <- fetchKey h j
-  mks <- fetchFrame h m >>= availableKeys h
+  mks <- fetchNode h m >>= availableKeys h
   vk <- fetchKey h k
-  rks <- fetchFrame h r >>= availableKeys h
+  rks <- fetchNode h r >>= availableKeys h
   return $ lks ++ vj : mks ++ vk : rks
 availableKeys _ _ = fail "availableKeys: unexpected frame"
 
+fetchPayload :: LisztHandle -> RawPointer -> IO B.ByteString
+fetchPayload LisztHandle{..} (RP ofs len) = do
+  hSeek hPayload AbsoluteSeek (fromIntegral ofs)
+  B.hGet hPayload len
+
 --------------------------------------------------------------------------------
 -- Footer (root node)
 
@@ -451,10 +508,10 @@
 emptyFooter = B.pack  [0,14,171,160,140,150,185,18,22,70,203,145,129,232,42,76,81,176,163,195,96,209,8,74,97,123,57,136,107,174,241,142,100,164,181,138,253,170,25,77,12,191,212,224,142,167,215,73,48,0,2,170,226,114,8,29,141,85,243,179,81,11,59,246,62,189,202,254,56,140,227,195,189,118,152,26,106,81,4,121,152,72,247,119,111,128,75,242,29,96,157,190,170,1,57,77,61,132,72,8,233,94,254,18,197,152,128,15,174,9,91,78,125,21,72,250,179,176,176,47,230,45,255,228,214,223,28,61,123,159,104,233,131,39,88,245,13,242,228,48,17,119,159,173,71,172,238,69,137,141,153,133,79,24,81,242,19,21,209,44,120,69,180,103,100,185,189,191,50,132,52,229,248,12,207,134,45,241,2,217,112,21,239,65,39,30,33,16,147,152,52,204,221,56,87,191,235,235,173,181,106,165,37,52,245,221,13,80,91,207,224,95,157,222,3,210,54,28,99,1,7,50,189,163,141,244,200,101,250,61,48,10,243,248,153,98,224,73,227,121,72,156,228,205,43,82,166,48,85,132,0,76,73,83,90,84]
 
 isFooter :: B.ByteString -> Bool
-isFooter bs = B.drop 128 bs == B.drop 128 emptyFooter
+isFooter bs = B.drop (footerSize - 64) bs == B.drop (footerSize - 64) emptyFooter
 
-fetchRoot :: LisztHandle -> IO (Frame RawPointer)
-fetchRoot h = fetchFrame'
+fetchRoot :: LisztHandle -> IO (Node Tag RawPointer)
+fetchRoot h = fetchNode'
   (hSeek (hPayload h) SeekFromEnd (-fromIntegral footerSize)) h footerSize
 
 --------------------------------------------------------------------------------
@@ -470,15 +527,15 @@
 dropSpine _ 0 s = return s
 dropSpine h n0 ((siz0, t0) : xs0)
   | siz0 <= n0 = dropSpine h (n0 - siz0) xs0
-  | otherwise = dropTree n0 siz0 t0 xs0
+  | otherwise = dropBin n0 siz0 t0 xs0
   where
-    dropTree 0 siz t xs = return $ (siz, t) : xs
-    dropTree n siz t xs = fetchFrame h t >>= \case
-      Tree _ _ l r
+    dropBin 0 siz t xs = return $ (siz, t) : xs
+    dropBin n siz t xs = fetchNode h t >>= \case
+      Bin _ _ l r
         | n == 1 -> return $ (siz', l) : (siz', r) : xs
-        | n <= siz' -> dropTree (n - 1) siz' l ((siz', r) : xs)
-        | otherwise -> dropTree (n - siz' - 1) siz' r xs
-      Leaf _ _ -> return xs
+        | n <= siz' -> dropBin (n - 1) siz' l ((siz', r) : xs)
+        | otherwise -> dropBin (n - siz' - 1) siz' r xs
+      Tip _ _ -> return xs
       _ -> error $ "dropTree: unexpected frame"
       where
         siz' = siz `div` 2
@@ -488,26 +545,30 @@
 takeSpine _ _ [] ps = return ps
 takeSpine h n ((siz, t) : xs) ps
   | n >= siz = takeAll h t ps >>= takeSpine h (n - siz) xs
-  | otherwise = takeTree h n siz t ps
+  | otherwise = takeBin h n siz t ps
 
-takeTree :: Fetchable a => LisztHandle -> Int -> Int -> a -> [QueryResult] -> IO [QueryResult]
-takeTree _ n _ _ ps | n <= 0 = return ps
-takeTree h n siz t ps = fetchFrame h t >>= \case
-  Tree tag p l r
+takeBin :: Fetchable a => LisztHandle -> Int -> Int -> a -> [QueryResult] -> IO [QueryResult]
+takeBin _ n _ _ ps | n <= 0 = return ps
+takeBin h n siz t ps = fetchNode h t >>= \case
+  Bin tag p l r
     | n == 1 -> return $ (tag, p) : ps
-    | n <= siz' -> takeTree h (n - 1) siz' l ((tag, p) : ps)
+    | n <= siz' -> takeBin h (n - 1) siz' l ((tag, p) : ps)
     | otherwise -> do
       ps' <- takeAll h l ((tag, p) : ps)
-      takeTree h (n - siz' - 1) siz' r ps'
-  Leaf tag p -> return $ (tag, p) : ps
+      takeBin h (n - siz' - 1) siz' r ps'
+  Tip tag p -> return $ (tag, p) : ps
   _ -> error $ "takeTree: unexpected frame"
   where
     siz' = siz `div` 2
 
+wholeSpine :: Fetchable a => LisztHandle -> Spine a -> [QueryResult] -> IO [QueryResult]
+wholeSpine h ((_, s) : ss) r = wholeSpine h ss r >>= takeAll h s
+wholeSpine _ [] r = return r
+
 takeAll :: Fetchable a => LisztHandle -> a -> [QueryResult] -> IO [QueryResult]
-takeAll h t ps = fetchFrame h t >>= \case
-  Tree tag p l r -> takeAll h l ((tag, p) : ps) >>= takeAll h r
-  Leaf tag p -> return ((tag, p) : ps)
+takeAll h t ps = fetchNode h t >>= \case
+  Bin tag p l r -> takeAll h l ((tag, p) : ps) >>= takeAll h r
+  Tip tag p -> return ((tag, p) : ps)
   _ -> error $ "takeAll: unexpected frame"
 
 takeSpineWhile :: Fetchable a
@@ -516,11 +577,11 @@
   -> Spine a
   -> [QueryResult] -> IO [QueryResult]
 takeSpineWhile cond h = go where
-  go (t0 : ((siz, t) : xs)) ps = fetchFrame h t >>= \case
-    Leaf tag p
+  go (t0 : ((siz, t) : xs)) ps = fetchNode h t >>= \case
+    Tip tag p
       | cond tag -> takeAll h (snd t0) ps >>= go xs . ((tag, p):)
       | otherwise -> inner t0 ps
-    Tree tag p l r
+    Bin tag p l r
       | cond tag -> takeAll h (snd t0) ps
         >>= go ((siz', l) : (siz', r) : xs) . ((tag, p):)
       | otherwise -> inner t0 ps
@@ -530,11 +591,11 @@
   go [t] ps = inner t ps
   go [] ps = return ps
 
-  inner (siz, t) ps = fetchFrame h t >>= \case
-    Leaf tag p
+  inner (siz, t) ps = fetchNode h t >>= \case
+    Tip tag p
       | cond tag -> return $ (tag, p) : ps
       | otherwise -> return ps
-    Tree tag p l r
+    Bin tag p l r
       | cond tag -> go [(siz', l), (siz', r)] ((tag, p) : ps)
       | otherwise -> return ps
     _ -> error "takeSpineWhile: unexpected frame"
@@ -547,50 +608,26 @@
   -> Spine a
   -> IO (Maybe (Int, QueryResult, Spine a))
 dropSpineWhile cond h = go 0 where
-  go !total (t0@(siz0, _) : ts@((siz, t) : xs)) = fetchFrame h t >>= \case
-    Leaf tag _
+  go !total (t0@(siz0, _) : ts@((siz, t) : xs)) = fetchNode h t >>= \case
+    Tip tag _
       | cond tag -> go (total + siz0 + siz) xs
-      | otherwise -> dropTree total t0 ts
-    Tree tag _ l r
+      | otherwise -> dropBin total t0 ts
+    Bin tag _ l r
       | cond tag -> go (total + siz0 + 1) $ (siz', l) : (siz', r) : xs
-      | otherwise -> dropTree total t0 ts
+      | otherwise -> dropBin total t0 ts
     _ -> error "dropSpineWhile: unexpected frame"
     where
       siz' = siz `div` 2
-  go total (t : ts) = dropTree total t ts
+  go total (t : ts) = dropBin total t ts
   go _ [] = return Nothing
 
-  dropTree !total (siz, t) ts = fetchFrame h t >>= \case
-    Leaf tag p
+  dropBin !total (siz, t) ts = fetchNode h t >>= \case
+    Tip tag p
       | cond tag -> go (total + 1) ts
       | otherwise -> return $ Just (total, (tag, p), ts)
-    Tree tag p l r
+    Bin tag p l r
       | cond tag -> go (total + 1) $ (siz', l) : (siz', r) : ts
       | otherwise -> return $ Just (total, (tag, p), (siz', l) : (siz', r) : ts)
     _ -> error "dropSpineWhile: unexpected frame"
     where
       siz' = siz `div` 2
-
---------------------------------------------------------------------------------
--- Magical
-
-copyByteString :: B.ByteString -> IO B.ByteString
-copyByteString (B.PS fp ofs len) = do
-  fp' <- B.mallocByteString len
-  withForeignPtr fp' $ \dst -> withForeignPtr fp
-    $ \src -> B.memcpy dst (src `plusPtr` ofs) len
-  return (B.PS fp' 0 len)
-
-forceSpine :: NFData a => Frame a -> IO (Frame a)
-forceSpine f = case f of
-  Empty -> return Empty
-  Leaf1 _ s -> rnf s `seq` return f
-  Leaf2 _ s _ t -> rnf s `seq` rnf t `seq` return f
-  Node2 _ _ s _ -> rnf s  `seq` return f
-  Node3 _ _ s _ _ t _ -> rnf s `seq` rnf t `seq` return f
-  Tree tag p l r -> do
-    tag' <- WB.bytes <$> copyByteString (WB.toByteString tag)
-    return (Tree tag' p l r)
-  Leaf tag p -> do
-    tag' <- WB.bytes <$> copyByteString (WB.toByteString tag)
-    return (Leaf tag' p)
diff --git a/src/Database/Liszt/Internal/Decoder.hs b/src/Database/Liszt/Internal/Decoder.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Liszt/Internal/Decoder.hs
@@ -0,0 +1,70 @@
+module Database.Liszt.Internal.Decoder (Decoder(..)
+  , runDecoder
+  , DecodeResult(..)
+  , decodeWord8
+  , decodeVarInt
+  , decodeInt) where
+
+import Control.Monad.Catch
+import Foreign.Ptr
+import Foreign.Storable
+import Data.Bits
+import Data.Word
+import System.Endian
+
+newtype Decoder a = Decoder { unDecoder :: Ptr Word8 -> IO (DecodeResult a) }
+
+runDecoder :: Decoder a -> Ptr Word8 -> IO a
+runDecoder m p = do
+  DecodeResult _ a <- unDecoder m p
+  return a
+
+data DecodeResult a = DecodeResult {-# UNPACK #-} !(Ptr Word8) !a
+
+instance MonadThrow Decoder where
+  throwM e = Decoder $ \_ -> throwM e
+
+instance Functor Decoder where
+  fmap f m = Decoder $ \p -> do
+    DecodeResult p' a <- unDecoder m p
+    return (DecodeResult p' (f a))
+  {-# INLINE fmap #-}
+
+instance Applicative Decoder where
+  pure a = Decoder $ \p -> pure $ DecodeResult p a
+  {-# INLINE pure #-}
+  m <*> n = Decoder $ \p -> do
+    DecodeResult p' f <- unDecoder m p
+    DecodeResult p'' a <- unDecoder n p'
+    return $ DecodeResult p'' (f a)
+  {-# INLINE (<*>) #-}
+
+instance Monad Decoder where
+  return = pure
+  m >>= k = Decoder $ \p -> do
+    DecodeResult p' a <- unDecoder m p
+    unDecoder (k a) p'
+  {-# INLINE (>>=) #-}
+  (>>) = (*>)
+
+decodeWord8 :: Decoder Word8
+decodeWord8 = Decoder $ \ptr -> do
+  a <- peek ptr
+  return $ DecodeResult (ptr `plusPtr` 1) a
+{-# INLINE decodeWord8 #-}
+
+decodeVarInt :: Decoder Int
+decodeVarInt = decodeWord8 >>= go
+  where
+    go n
+      | testBit n 7 = do
+        m <- decodeWord8 >>= go
+        return $! unsafeShiftL m 7 .|. clearBit (fromIntegral n) 7
+      | otherwise = return $ fromIntegral n
+{-# INLINE decodeVarInt #-}
+
+decodeInt :: Decoder Int
+decodeInt = Decoder $ \ptr -> do
+  a <- peek (castPtr ptr)
+  return $ DecodeResult (ptr `plusPtr` 8) (fromIntegral $ fromBE64 a)
+{-# INLINE decodeInt #-}
diff --git a/src/Database/Liszt/Network.hs b/src/Database/Liszt/Network.hs
--- a/src/Database/Liszt/Network.hs
+++ b/src/Database/Liszt/Network.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE LambdaCase, OverloadedStrings #-}
 module Database.Liszt.Network
   ( startServer
   , Connection
@@ -8,40 +8,44 @@
   , fetch) where
 
 import Control.Concurrent
-import Control.Exception
+import Control.Exception (evaluate, throw, throwIO)
 import Control.Monad
+import Control.Monad.Catch
+import Control.Monad.IO.Class
 import Database.Liszt.Tracker
 import Database.Liszt.Internal (hPayload, RawPointer(..))
-import Data.Binary
-import Data.Binary.Get
+import Data.Serialize
+import Data.Serialize.Get
 import Data.Winery
 import qualified Data.Winery.Internal.Builder as WB
 import qualified Data.ByteString.Char8 as B
-import qualified Data.ByteString.Lazy as BL
 import qualified Network.Socket.SendFile.Handle as SF
 import qualified Network.Socket.ByteString as SB
 import qualified Network.Socket as S
+import System.FilePath ((</>))
 import System.IO
 import Text.Read (readMaybe)
 
 respond :: Tracker -> S.Socket -> IO ()
 respond tracker conn = do
   msg <- SB.recv conn 4096
-  req <- try (evaluate $ decodeCurrent msg) >>= \case
-    Left e -> throwIO $ WineryError e
-    Right a -> return a
-  unless (B.null msg) $ handleRequest tracker req $ \lh lastSeqNo offsets -> do
-    let count = length offsets
-    _ <- SB.send conn $ encodeResp $ Right count
-    forM_ (zip [lastSeqNo - count + 1..] offsets) $ \(i, (tag, RP pos len)) -> do
-      SB.sendAll conn $ WB.toByteString $ mconcat
-        [ WB.word64 (fromIntegral i)
-        , WB.word64 (fromIntegral $ WB.getSize tag), tag
-        , WB.word64 $ fromIntegral len]
-      SF.sendFile' conn (hPayload lh) (fromIntegral pos) (fromIntegral len)
+  unless (B.null msg) $ do
+    req <- try (evaluate $ decodeCurrent msg) >>= \case
+      Left e -> throwIO $ WineryError e
+      Right a -> return a
+    handleRequest tracker req $ \lh lastSeqNo offsets -> do
+      let count = length offsets
+      _ <- SB.send conn $ encodeResp $ Right count
+      forM_ (zip [lastSeqNo - count + 1..] offsets) $ \(i, (tag, RP pos len)) -> do
+        SB.sendAll conn $ WB.toByteString $ mconcat
+          [ WB.word64 (fromIntegral i)
+          , WB.word64 (fromIntegral $ B.length tag), WB.bytes tag
+          , WB.word64 $ fromIntegral len]
+        SF.sendFile' conn (hPayload lh) (fromIntegral pos) (fromIntegral len)
+    respond tracker conn
 
 startServer :: Int -> FilePath -> IO ()
-startServer port prefix = withLisztReader prefix $ \env -> do
+startServer port prefix = withLisztReader $ \env -> do
   let hints = S.defaultHints { S.addrFlags = [S.AI_NUMERICHOST, S.AI_NUMERICSERV], S.addrSocketType = S.Stream }
   addr:_ <- S.getAddrInfo (Just hints) (Just "0.0.0.0") (Just $ show port)
   bracket (S.socket (S.addrFamily addr) (S.addrSocketType addr) (S.addrProtocol addr)) S.close $ \sock -> do
@@ -52,10 +56,13 @@
     forever $ do
       (conn, _) <- S.accept sock
       forkFinally (do
-        path <- decode .  BL.fromStrict <$> SB.recv conn 4096
-        withTracker env path $ \t -> do
+        bs <- SB.recv conn 4096
+        path <- case decode bs of
+          Right a -> return a
+          Left _ -> throwM InvalidRequest
+        withTracker env (prefix </> path) $ \t -> do
           SB.sendAll conn $ B.pack "READY"
-          forever $ respond t conn)
+          respond t conn)
         $ \result -> do
           case result of
             Left ex -> case fromException ex of
@@ -65,37 +72,43 @@
           S.close conn
 
 encodeResp :: Either String Int -> B.ByteString
-encodeResp = BL.toStrict . encode
+encodeResp = encode
 
 newtype Connection = Connection (MVar S.Socket)
 
-withConnection :: String -> Int -> B.ByteString -> (Connection -> IO r) -> IO r
+withConnection :: (MonadIO m, MonadMask m) => String -> Int -> B.ByteString -> (Connection -> m r) -> m r
 withConnection host port path = bracket (connect host port path) disconnect
 
-connect :: String -> Int -> B.ByteString -> IO Connection
-connect host port path = do
+connect :: MonadIO m => String -> Int -> B.ByteString -> m Connection
+connect host port path = liftIO $ do
   let hints = S.defaultHints { S.addrFlags = [S.AI_NUMERICSERV], S.addrSocketType = S.Stream }
   addr:_ <- S.getAddrInfo (Just hints) (Just host) (Just $ show port)
   sock <- S.socket (S.addrFamily addr) (S.addrSocketType addr) (S.addrProtocol addr)
   S.connect sock $ S.addrAddress addr
-  SB.sendAll sock $ BL.toStrict $ encode path
-  _ <- SB.recv sock 4096
-  Connection <$> newMVar sock
+  SB.sendAll sock $ encode path
+  resp <- SB.recv sock 4096
+  case resp of
+    "READY" -> Connection <$> newMVar sock
+    e -> case readMaybe <$> decode e of
+      Right (Just (Left e')) -> throw (e' :: LisztError)
+      Right (Just (Right ())) -> fail $ "connect: Unexpected response: " ++ show e
+      _ -> fail $ "connect: Unexpected response: " ++ show e
 
-disconnect :: Connection -> IO ()
-disconnect (Connection sock) = takeMVar sock >>= S.close
+disconnect :: MonadIO m => Connection -> m ()
+disconnect (Connection sock) = liftIO $ takeMVar sock >>= S.close
 
-fetch :: Connection -> Request -> IO [(Int, B.ByteString, B.ByteString)]
-fetch (Connection msock) req = modifyMVar msock $ \sock -> do
+fetch :: MonadIO m => Connection -> Request -> m [(Int, B.ByteString, B.ByteString)]
+fetch (Connection msock) req = liftIO $ modifyMVar msock $ \sock -> do
   SB.sendAll sock $ serialiseOnly req
-  go sock $ runGetIncremental $ get >>= \case
+  bs <- SB.recv sock 4096
+  go sock $ flip runGetPartial bs $ get >>= \case
     Left e -> case readMaybe e of
       Just e' -> throw (e' :: LisztError)
       Nothing -> fail $ "Unknown error: " ++ show e
     Right n -> replicateM n ((,,) <$> get <*> get <*> get)
   where
-    go sock (Done _ _ a) = return (sock, a)
+    go sock (Done a _) = return (sock, a)
     go sock (Partial cont) = do
       bs <- SB.recv sock 4096
-      if B.null bs then go sock $ cont Nothing else go sock $ cont $ Just bs
-    go _ (Fail _ _ str) = fail $ show req ++ ": " ++ str
+      if B.null bs then go sock $ cont "" else go sock $ cont bs
+    go _ (Fail str _) = fail $ show req ++ ": " ++ str
diff --git a/src/Database/Liszt/Tracker.hs b/src/Database/Liszt/Tracker.hs
--- a/src/Database/Liszt/Tracker.hs
+++ b/src/Database/Liszt/Tracker.hs
@@ -32,7 +32,6 @@
 import Data.Text (Text)
 import Data.Winery
 import Foreign.ForeignPtr
-import qualified Data.Winery.Internal.Builder as WB
 import GHC.Generics (Generic)
 import System.Directory
 import System.FilePath
@@ -74,25 +73,25 @@
 instance Exception LisztError
 
 data Tracker = Tracker
-  { vRoot :: !(TVar (Frame CachePointer))
+  { vRoot :: !(TVar (Node Tag CachePointer))
   , vUpdated :: !(TVar Bool)
   , vPending :: !(TVar [STM (IO ())])
   , vReaders :: !(TVar Int)
   , followThread :: !ThreadId
-  , trackerPath :: !B.ByteString
+  , filePath :: !FilePath
   , streamHandle :: !LisztHandle
   , cache :: !Cache
   }
 
 data Cache = Cache
-  { primaryCache :: TVar (IM.IntMap (Frame CachePointer))
-  , secondaryCache :: TVar (IM.IntMap (Frame CachePointer))
+  { primaryCache :: TVar (IM.IntMap (Node Tag CachePointer))
+  , secondaryCache :: TVar (IM.IntMap (Node Tag CachePointer))
   }
 
 newtype CachePointer = CachePointer RawPointer
 
 instance Given Cache => Fetchable CachePointer where
-  fetchFrame h (CachePointer p@(RP ofs _)) = join $ atomically $ do
+  fetchNode h (CachePointer p@(RP ofs _)) = join $ atomically $ do
     let Cache{..} = given
     pcache <- readTVar primaryCache
     case IM.lookup ofs pcache of
@@ -104,7 +103,7 @@
             writeTVar primaryCache $! IM.insert ofs x pcache
             return (pure x)
           Nothing -> return $ do
-            x <- fmap CachePointer <$> fetchFrame h p
+            x <- fmap CachePointer <$> fetchNode h p
             atomically $ modifyTVar' primaryCache $ IM.insert ofs x
             return x
 
@@ -113,9 +112,8 @@
   readTVar primaryCache >>= writeTVar secondaryCache
   writeTVar primaryCache IM.empty
 
-createTracker :: WatchManager -> FilePath -> B.ByteString -> IO Tracker
-createTracker man prefix trackerPath = do
-  let filePath = prefix </> B.unpack trackerPath
+createTracker :: WatchManager -> FilePath -> IO Tracker
+createTracker man filePath = do
   exist <- doesFileExist filePath
   unless exist $ throwIO FileNotFound
   streamHandle <- openLiszt filePath
@@ -128,34 +126,27 @@
     _ -> False)
     $ const $ void $ atomically $ writeTVar vUpdated True
 
-  fptr <- B.mallocByteString 4096
-
   let wait = atomically $ do
         b <- readTVar vUpdated
         unless b retry
         writeTVar vUpdated False
 
-  hSeek (hPayload streamHandle) SeekFromEnd (-fromIntegral footerSize)
-  let seekRoot prevSize = do
-        n <- withForeignPtr fptr $ \p -> hGetBuf (hPayload streamHandle) p 4096
-        if n == 0
-          then do
-            let bs = B.PS fptr (max 0 $ prevSize - footerSize) footerSize
-            if isFooter bs
-              then try (evaluate $ decodeFrame bs) >>= \case
-                Left (_ :: DecodeException) -> do
-                  wait
-                  seekRoot 0
-                Right a -> forceSpine a
-              else do
-                wait
-                seekRoot 0
-          else seekRoot n
+  let seekRoot = do
+        hSeek (hPayload streamHandle) SeekFromEnd (-fromIntegral footerSize)
+        bs@(B.PS fp _ _) <- B.hGet (hPayload streamHandle) footerSize
+        if isFooter bs
+          then try (withForeignPtr fp peekNode) >>= \case
+            Left LisztDecodingException -> do
+              putStrLn "WARNING: isFooter passed, but it's not valid"
+              wait
+              seekRoot
+            Right a -> return a
+          else wait >> seekRoot
 
   cache <- Cache <$> newTVarIO IM.empty <*> newTVarIO IM.empty
 
   followThread <- forkFinally (forever $ do
-    newRoot <- fmap CachePointer <$> seekRoot 0
+    newRoot <- fmap CachePointer <$> seekRoot
     join $ atomically $ do
       flipCache cache
       writeTVar vRoot newRoot
@@ -169,16 +160,15 @@
 
 data LisztReader = LisztReader
   { watchManager :: WatchManager
-  , vTrackers :: TVar (HM.HashMap B.ByteString Tracker)
-  , prefix :: FilePath
+  , vTrackers :: TVar (HM.HashMap FilePath Tracker)
   }
 
-withLisztReader :: FilePath -> (LisztReader -> IO ()) -> IO ()
-withLisztReader prefix k = do
+withLisztReader :: (LisztReader -> IO ()) -> IO ()
+withLisztReader k = do
   vTrackers <- newTVarIO HM.empty
   withManager $ \watchManager -> k LisztReader{..}
 
-acquireTracker :: LisztReader -> B.ByteString -> IO Tracker
+acquireTracker :: LisztReader -> FilePath -> IO Tracker
 acquireTracker LisztReader{..} path = join $ atomically $ do
   streams <- readTVar vTrackers
   case HM.lookup path streams of
@@ -186,7 +176,7 @@
       modifyTVar' (vReaders s) (+1)
       return (return s)
     Nothing -> return $ do
-      s <- createTracker watchManager prefix path
+      s <- createTracker watchManager path
       atomically $ modifyTVar vTrackers (HM.insert path s)
       return s
 
@@ -195,13 +185,13 @@
   n <- readTVar vReaders
   if n <= 1
     then do
-      modifyTVar' vTrackers (HM.delete trackerPath)
+      modifyTVar' vTrackers (HM.delete filePath)
       return $ do
         killThread followThread
         closeLiszt streamHandle
     else return () <$ writeTVar vReaders (n - 1)
 
-withTracker :: LisztReader -> B.ByteString -> (Tracker -> IO a) -> IO a
+withTracker :: LisztReader -> FilePath -> (Tracker -> IO a) -> IO a
 withTracker env path = bracket (acquireTracker env path) (releaseTracker env)
 
 handleRequest :: Tracker
@@ -230,20 +220,20 @@
                 SeqNo n -> takeSpine streamHandle (min reqLimit $ ofs - n + 1) spine' [] >>= cont streamHandle ofs
                 WineryTag sch name p -> do
                   dec <- handleWinery sch name
-                  takeSpineWhile ((>=p) . dec . WB.toByteString) streamHandle spine' [] >>= cont streamHandle ofs
+                  takeSpineWhile ((>=p) . dec) streamHandle spine' [] >>= cont streamHandle ofs
       case reqTo of
         FromEnd ofs -> goSeqNo (len - ofs)
         SeqNo ofs -> goSeqNo ofs
         WineryTag sch name p -> do
           dec <- handleWinery sch name
-          dropSpineWhile ((>=p) . dec . WB.toByteString) streamHandle spine >>= \case
+          dropSpineWhile ((>=p) . dec) streamHandle spine >>= \case
             Nothing -> cont streamHandle 0 []
             Just (dropped, e, spine') -> case reqFrom of
               FromEnd n -> takeSpine streamHandle (min reqLimit $ n - dropped + 1) spine' [e] >>= cont streamHandle (len - dropped)
               SeqNo n -> takeSpine streamHandle (min reqLimit $ len - dropped - n + 1) spine' [e] >>= cont streamHandle (len - dropped)
               WineryTag sch' name' q -> do
                 dec' <- handleWinery sch' name'
-                takeSpineWhile ((>=q) . dec' . WB.toByteString) streamHandle spine' [e] >>= cont streamHandle (len - dropped)
+                takeSpineWhile ((>=q) . dec') streamHandle spine' [e] >>= cont streamHandle (len - dropped)
   where
     handleWinery :: Schema -> [Text] -> IO (B.ByteString -> Scientific)
     handleWinery sch names = either (throwIO . WinerySchemaError . show) pure
