diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,9 @@
 # Changelog for Calamity
 
+## 0.1.2.0
+
+*2020-04-27*
+
+* Calamity.Client: runBotIO now has a `Polysemy.Fail` effect
+
 ## Unreleased changes
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@
 
 import           Calamity
 import           Calamity.Cache.InMemory
+import           Calamity.Metrics.Noop
 
 import           Control.Concurrent
 import           Control.Concurrent.STM.TVar
@@ -55,35 +56,42 @@
   P.runAtomicStateTVar var $ P.reinterpret (\case
                                               GetCounter -> P.atomicState (\v -> (v + 1, v))) m
 
-handleErrorByLogging m = do
+handleFailByLogging m = do
   r <- P.runFail m
   case r of
     Left e -> DiP.error (e ^. packed)
     _      -> pure ()
 
+handleFailByPrinting m = do
+  r <- P.runFail m
+  case r of
+    Left e -> P.embed $ print (show e)
+    _      -> pure ()
+
 info = DiP.info @Text
 debug = DiP.info @Text
 
 main :: IO ()
 main = do
-  P.runFinal . P.embedToFinal . runCounterAtomic . runCacheInMemory $ runBotIO (BotToken "") $ do
-    react @"messagecreate" $ \msg -> handleErrorByLogging $ do
-      when (msg ^. #content == "!count") $ replicateM_ 3 $ do
-        val <- getCounter
-        info $ "the counter is: " <> fromStrict (showt val)
-        void . invokeRequest $ CreateMessage (msg ^. #channelID) ("The value is: " <> showt val)
-      when (msg ^. #content == "!say hi") $ replicateM_ 3 . P.async $ do
-        info "saying heya"
-        Right msg' <- invokeRequest $ CreateMessage (msg ^. #channelID) "heya"
-        info "sleeping"
-        P.embed $ threadDelay (5 * 1000 * 1000)
-        info "slept"
-        void . invokeRequest $ EditMessage (msg ^. #channelID) msg' (Just "lol") Nothing
-        info "edited"
-      when (msg ^. #content == "!explode") $ do
-        Just x <- pure Nothing
-        debug "unreachable!"
-      when (msg ^. #content == "!bye") $ do
-        void . invokeRequest $ CreateMessage (msg ^. #channelID) "bye!"
-        stopBot
+  P.runFinal . P.embedToFinal . handleFailByPrinting . runCounterAtomic . runCacheInMemory . runMetricsNoop $ runBotIO
+    (BotToken "") $ do
+      react @"messagecreate" $ \msg -> handleFailByLogging $ do
+        when (msg ^. #content == "!count") $ replicateM_ 3 $ do
+          val <- getCounter
+          info $ "the counter is: " <> fromStrict (showt val)
+          void . invokeRequest $ CreateMessage (msg ^. #channelID) ("The value is: " <> showt val)
+        when (msg ^. #content == "!say hi") $ replicateM_ 3 . P.async $ do
+          info "saying heya"
+          Right msg' <- invokeRequest $ CreateMessage (msg ^. #channelID) "heya"
+          info "sleeping"
+          P.embed $ threadDelay (5 * 1000 * 1000)
+          info "slept"
+          void . invokeRequest $ EditMessage (msg ^. #channelID) msg' (Just "lol") Nothing
+          info "edited"
+        when (msg ^. #content == "!explode") $ do
+          Just x <- pure Nothing
+          debug "unreachable!"
+        when (msg ^. #content == "!bye") $ do
+          void . invokeRequest $ CreateMessage (msg ^. #channelID) "bye!"
+          stopBot
 ```
diff --git a/cabal.project b/cabal.project
--- a/cabal.project
+++ b/cabal.project
@@ -1,6 +1,6 @@
 -- Generated by stackage-to-hackage
 
-with-compiler: ghc-8.6.5
+with-compiler: ghc-8.8.3
 
 packages:
     ./
diff --git a/calamity.cabal b/calamity.cabal
--- a/calamity.cabal
+++ b/calamity.cabal
@@ -1,145 +1,143 @@
 cabal-version: 1.18
-name: calamity
-version: 0.1.0.0
-license: MIT
-license-file: LICENSE
-copyright: 2020 Ben Simms
-maintainer: ben@bensimms.moe
-author: Ben Simms
-tested-with: ghc ==8.6.5
-homepage: https://github.com/nitros12/calamity
-bug-reports: https://github.com/nitros12/calamity/issues
-synopsis: A library for writing discord bots
-description:
-    Please see the README on GitHub at <https://github.com/nitros12/calamity#readme>
-category: Network, Web
-build-type: Simple
+
+-- This file has been generated from package.yaml by hpack version 0.31.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: d58d4118390485680eba20c1d9dbc30df68a2f58ff358e7fd5f1a04be6a5b3c0
+
+name:           calamity
+version:        0.1.2.0
+synopsis:       A library for writing discord bots
+description:    Please see the README on GitHub at <https://github.com/nitros12/calamity#readme>
+category:       Network, Web
+homepage:       https://github.com/nitros12/calamity
+bug-reports:    https://github.com/nitros12/calamity/issues
+author:         Ben Simms
+maintainer:     ben@bensimms.moe
+copyright:      2020 Ben Simms
+license:        MIT
+license-file:   LICENSE
+tested-with:    GHC == 8.6.5, GHC == 8.8.3
+build-type:     Simple
 extra-source-files:
     README.md
     ChangeLog.md
     cabal.project
-extra-doc-files: README.md
+extra-doc-files:
+    README.md
 
 source-repository head
-    type: git
-    location: https://github.com/nitros12/calamity
+  type: git
+  location: https://github.com/nitros12/calamity
 
 library
-    exposed-modules:
-        Calamity
-        Calamity.Cache.Eff
-        Calamity.Cache.InMemory
-        Calamity.Client
-        Calamity.Client.Client
-        Calamity.Client.ShardManager
-        Calamity.Client.Types
-        Calamity.Gateway
-        Calamity.Gateway.DispatchEvents
-        Calamity.Gateway.Shard
-        Calamity.Gateway.Types
-        Calamity.HTTP
-        Calamity.HTTP.Channel
-        Calamity.HTTP.Emoji
-        Calamity.HTTP.Guild
-        Calamity.HTTP.Internal.Ratelimit
-        Calamity.HTTP.Internal.Request
-        Calamity.HTTP.Internal.Route
-        Calamity.HTTP.Internal.Types
-        Calamity.HTTP.MiscRoutes
-        Calamity.Internal.AesonThings
-        Calamity.Internal.MessageStore
-        Calamity.Internal.SnowflakeMap
-        Calamity.Internal.Updateable
-        Calamity.Internal.Utils
-        Calamity.LogEff
-        Calamity.Types
-        Calamity.Types.Model
-        Calamity.Types.Model.Channel
-        Calamity.Types.Model.Channel.Attachment
-        Calamity.Types.Model.Channel.ChannelType
-        Calamity.Types.Model.Channel.DM
-        Calamity.Types.Model.Channel.Embed
-        Calamity.Types.Model.Channel.Group
-        Calamity.Types.Model.Channel.Guild
-        Calamity.Types.Model.Channel.Guild.Category
-        Calamity.Types.Model.Channel.Guild.Text
-        Calamity.Types.Model.Channel.Guild.Voice
-        Calamity.Types.Model.Channel.Message
-        Calamity.Types.Model.Channel.Reaction
-        Calamity.Types.Model.Guild
-        Calamity.Types.Model.Guild.Emoji
-        Calamity.Types.Model.Guild.Guild
-        Calamity.Types.Model.Guild.Invite
-        Calamity.Types.Model.Guild.Member
-        Calamity.Types.Model.Guild.Overwrite
-        Calamity.Types.Model.Guild.Role
-        Calamity.Types.Model.Guild.UnavailableGuild
-        Calamity.Types.Model.Presence
-        Calamity.Types.Model.Presence.Activity
-        Calamity.Types.Model.Presence.Presence
-        Calamity.Types.Model.User
-        Calamity.Types.Model.Voice
-        Calamity.Types.Model.Voice.VoiceState
-        Calamity.Types.Partial
-        Calamity.Types.Snowflake
-        Calamity.Types.Token
-        Calamity.Types.UnixTimestamp
-    hs-source-dirs: src
-    other-modules:
-        Paths_calamity
-    default-language: Haskell2010
-    default-extensions: CPP AllowAmbiguousTypes BlockArguments
-                        NoMonomorphismRestriction AutoDeriveTypeable BangPatterns
-                        BinaryLiterals UndecidableInstances ConstraintKinds DataKinds
-                        DefaultSignatures DeriveDataTypeable DeriveFoldable DeriveFunctor
-                        DeriveGeneric DeriveTraversable DoAndIfThenElse EmptyDataDecls
-                        ExistentialQuantification FlexibleContexts FlexibleInstances
-                        FunctionalDependencies GADTs DerivingVia DerivingStrategies
-                        GeneralizedNewtypeDeriving StandaloneDeriving DeriveAnyClass
-                        InstanceSigs KindSignatures LambdaCase MonadFailDesugaring
-                        MultiParamTypeClasses MultiWayIf NamedFieldPuns OverloadedStrings
-                        OverloadedLabels PartialTypeSignatures PatternGuards PolyKinds
-                        RankNTypes RecordWildCards ScopedTypeVariables TupleSections
-                        TypeFamilies TypeSynonymInstances ViewPatterns
-                        DuplicateRecordFields TypeOperators TypeApplications
-                        RoleAnnotations
-    ghc-options: -fplugin=Polysemy.Plugin -funbox-strict-fields -Wall
-    build-depends:
-        aeson >=1.4 && <2,
-        async >=2.2 && <3,
-        base >=4.12 && <5,
-        bytestring ==0.10.*,
-        concurrent-extra ==0.7.*,
-        data-default-class ==0.1.*,
-        deepseq >=1.4.4.0 && <2,
-        deque ==0.4.*,
-        df1 ==0.3.*,
-        di-polysemy ==0.1.*,
-        exceptions ==0.10.*,
-        fmt ==0.6.*,
-        focus >=1.0 && <2,
-        generic-lens >=2.0 && <3,
-        generic-override >=0.0.0.0 && <0.0.1,
-        generic-override-aeson >=0.0.0.0 && <0.0.1,
-        hashable >=1.2 && <2,
-        http-date >=0.0.8 && <0.1,
-        http-types ==0.12.*,
-        lens >=4.18 && <5,
-        lens-aeson >=1.1 && <2,
-        mtl >=2.2 && <3,
-        polysemy >=1.3 && <2,
-        polysemy-plugin ==0.2.*,
-        reflection >=2.1 && <3,
-        scientific ==0.3.*,
-        stm >=2.5 && <3,
-        stm-chans >=3.0 && <4,
-        stm-containers >=1.1 && <2,
-        text >=1.2 && <2,
-        text-show >=3.8 && <4,
-        time >=1.8 && <1.11,
-        typerep-map ==0.3.*,
-        unordered-containers ==0.2.*,
-        vector ==0.12.*,
-        websockets ==0.12.*,
-        wreq-patchable >=1.0 && <2,
-        wuss >=1.1 && <2
+  exposed-modules:
+      Calamity
+      Calamity.Cache.Eff
+      Calamity.Cache.InMemory
+      Calamity.Client
+      Calamity.Client.Client
+      Calamity.Client.ShardManager
+      Calamity.Client.Types
+      Calamity.Gateway
+      Calamity.Gateway.DispatchEvents
+      Calamity.Gateway.Shard
+      Calamity.Gateway.Types
+      Calamity.HTTP
+      Calamity.HTTP.Channel
+      Calamity.HTTP.Emoji
+      Calamity.HTTP.Guild
+      Calamity.HTTP.Internal.Ratelimit
+      Calamity.HTTP.Internal.Request
+      Calamity.HTTP.Internal.Route
+      Calamity.HTTP.Internal.Types
+      Calamity.HTTP.MiscRoutes
+      Calamity.Internal.AesonThings
+      Calamity.Internal.MessageStore
+      Calamity.Internal.SnowflakeMap
+      Calamity.Internal.Updateable
+      Calamity.Internal.Utils
+      Calamity.LogEff
+      Calamity.Metrics.Eff
+      Calamity.Metrics.Internal
+      Calamity.Metrics.Noop
+      Calamity.Types
+      Calamity.Types.Model
+      Calamity.Types.Model.Channel
+      Calamity.Types.Model.Channel.Attachment
+      Calamity.Types.Model.Channel.ChannelType
+      Calamity.Types.Model.Channel.DM
+      Calamity.Types.Model.Channel.Embed
+      Calamity.Types.Model.Channel.Group
+      Calamity.Types.Model.Channel.Guild
+      Calamity.Types.Model.Channel.Guild.Category
+      Calamity.Types.Model.Channel.Guild.Text
+      Calamity.Types.Model.Channel.Guild.Voice
+      Calamity.Types.Model.Channel.Message
+      Calamity.Types.Model.Channel.Reaction
+      Calamity.Types.Model.Guild
+      Calamity.Types.Model.Guild.Emoji
+      Calamity.Types.Model.Guild.Guild
+      Calamity.Types.Model.Guild.Invite
+      Calamity.Types.Model.Guild.Member
+      Calamity.Types.Model.Guild.Overwrite
+      Calamity.Types.Model.Guild.Role
+      Calamity.Types.Model.Guild.UnavailableGuild
+      Calamity.Types.Model.Presence
+      Calamity.Types.Model.Presence.Activity
+      Calamity.Types.Model.Presence.Presence
+      Calamity.Types.Model.User
+      Calamity.Types.Model.Voice
+      Calamity.Types.Model.Voice.VoiceState
+      Calamity.Types.Partial
+      Calamity.Types.Snowflake
+      Calamity.Types.Token
+      Calamity.Types.UnixTimestamp
+  other-modules:
+      Paths_calamity
+  hs-source-dirs:
+      src
+  default-extensions: CPP AllowAmbiguousTypes BlockArguments NoMonomorphismRestriction BangPatterns BinaryLiterals UndecidableInstances ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable DoAndIfThenElse EmptyDataDecls ExistentialQuantification FlexibleContexts FlexibleInstances FunctionalDependencies GADTs DerivingVia DerivingStrategies GeneralizedNewtypeDeriving StandaloneDeriving DeriveAnyClass InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns OverloadedStrings OverloadedLabels PartialTypeSignatures PatternGuards PolyKinds RankNTypes RecordWildCards ScopedTypeVariables TupleSections TypeFamilies TypeSynonymInstances ViewPatterns DuplicateRecordFields TypeOperators TypeApplications RoleAnnotations
+  ghc-options: -fplugin=Polysemy.Plugin -funbox-strict-fields -Wall
+  build-depends:
+      aeson >=1.4 && <2
+    , async >=2.2 && <3
+    , base >=4.12 && <5
+    , bytestring >=0.10 && <0.11
+    , concurrent-extra >=0.7 && <0.8
+    , containers >=0.6 && <0.7
+    , data-default-class >=0.1 && <0.2
+    , deepseq >=1.4.4.0 && <2
+    , deque >=0.4 && <0.5
+    , df1 >=0.3 && <0.4
+    , di-polysemy >=0.1.1 && <0.2
+    , exceptions >=0.10 && <0.11
+    , fmt >=0.6 && <0.7
+    , focus >=1.0 && <2
+    , generic-lens >=2.0 && <3
+    , generic-override >=0.0.0.0 && <0.0.1
+    , generic-override-aeson >=0.0.0.0 && <0.0.1
+    , hashable >=1.2 && <2
+    , http-date >=0.0.8 && <0.1
+    , http-types >=0.12 && <0.13
+    , lens >=4.18 && <5
+    , lens-aeson >=1.1 && <2
+    , mtl >=2.2 && <3
+    , polysemy >=1.3 && <2
+    , polysemy-plugin >=0.2 && <0.3
+    , reflection >=2.1 && <3
+    , scientific >=0.3 && <0.4
+    , stm >=2.5 && <3
+    , stm-chans >=3.0 && <4
+    , stm-containers >=1.1 && <2
+    , text >=1.2 && <2
+    , text-show >=3.8 && <4
+    , time >=1.8 && <1.11
+    , typerep-map >=0.3 && <0.4
+    , unordered-containers >=0.2 && <0.3
+    , vector >=0.12 && <0.13
+    , websockets >=0.12 && <0.13
+    , wreq-patchable >=1.0 && <2
+    , wuss >=1.1 && <2
+  default-language: Haskell2010
diff --git a/src/Calamity/Client/Client.hs b/src/Calamity/Client/Client.hs
--- a/src/Calamity/Client/Client.hs
+++ b/src/Calamity/Client/Client.hs
@@ -14,7 +14,7 @@
 import qualified Calamity.Internal.SnowflakeMap              as SM
 import           Calamity.Internal.Updateable
 import           Calamity.Internal.Utils
-import           Calamity.LogEff
+import           Calamity.Metrics.Eff
 import           Calamity.Types.Model.Channel
 import           Calamity.Types.Model.Guild.UnavailableGuild
 import           Calamity.Types.Model.Presence               ( Presence(..) )
@@ -39,7 +39,6 @@
 
 import           GHC.TypeLits
 
-import           Polysemy                                    ( Sem )
 import qualified Polysemy                                    as P
 import qualified Polysemy.Async                              as P
 import qualified Polysemy.AtomicState                        as P
@@ -61,9 +60,7 @@
                 rlState'
                 eventQueue'
 
-type SetupEff r = Sem (LogEff ': P.Reader Client ': P.AtomicState EventHandlers ': P.Async ': r) ()
-
-runBotIO :: (P.Members '[P.Embed IO, P.Final IO, CacheEff] r, Typeable r) => Token -> SetupEff r -> Sem r ()
+runBotIO :: (P.Members '[P.Embed IO, P.Final IO, P.Fail, CacheEff, MetricEff] r, Typeable r) => Token -> SetupEff r -> P.Sem r ()
 runBotIO token setup = do
   client <- P.embed $ newClient token
   handlers <- P.embed $ newTVarIO def
@@ -73,12 +70,12 @@
     clientLoop
     finishUp
 
-react :: forall (s :: Symbol) r. (KnownSymbol s, BotC r, EHType' s ~ Dynamic, Typeable (EHType s (Sem r))) => EHType s (Sem r) -> Sem r ()
+react :: forall (s :: Symbol) r. (KnownSymbol s, BotC r, EHType' s ~ Dynamic, Typeable (EHType s (P.Sem r))) => EHType s (P.Sem r) -> P.Sem r ()
 react f =
   let handlers = EventHandlers . TM.one $ EH @s [toDyn f]
   in P.atomicModify (handlers <>)
 
-stopBot :: BotC r => Sem r ()
+stopBot :: BotC r => P.Sem r ()
 stopBot = do
   debug "stopping bot"
   shards <- P.asks (^. #shards) >>= P.embed . readTVarIO
@@ -87,7 +84,7 @@
   eventQueue <- P.asks (^. #eventQueue)
   P.embed . atomically $ writeTQueue eventQueue ShutDown
 
-finishUp :: BotC r => Sem r ()
+finishUp :: BotC r => P.Sem r ()
 finishUp = do
   debug "finishing up"
   shards <- P.asks (^. #shards) >>= P.embed . readTVarIO
@@ -96,7 +93,7 @@
 
 -- | main loop of the client, handles fetching the next event, processing the event
 -- and invoking it's handler functions
-clientLoop :: BotC r => Sem r ()
+clientLoop :: BotC r => P.Sem r ()
 clientLoop = do
   evtQueue <- P.asks (^. #eventQueue)
   void . P.runError . forever $ do
@@ -106,7 +103,7 @@
       ShutDown          -> P.throw ()
   debug "leaving client loop"
 
-handleEvent :: BotC r => DispatchData -> Sem r ()
+handleEvent :: BotC r => DispatchData -> P.Sem r ()
 handleEvent data' = do
   debug "handling an event"
   eventHandlers <- P.atomicGet
@@ -125,9 +122,9 @@
 --       event handlers
 
 unwrapEvent :: forall s r.
-            (KnownSymbol s, EHType' s ~ Dynamic, Typeable r, Typeable (EHType s (Sem r)))
+            (KnownSymbol s, EHType' s ~ Dynamic, Typeable r, Typeable (EHType s (P.Sem r)))
             => EventHandlers
-            -> [EHType s (Sem r)]
+            -> [EHType s (P.Sem r)]
 unwrapEvent (EventHandlers eh) = map (fromJust . fromDynamic) . unwrapEventHandler @s . fromJust
   $ (TM.lookup eh :: Maybe (EventHandler s))
 -- where unwrapEach handler =
@@ -137,7 +134,7 @@
 handleEvent' :: BotC r
               => EventHandlers
               -> DispatchData
-              -> Sem (P.Fail ': r) [Sem r ()]
+              -> P.Sem (P.Fail ': r) [P.Sem r ()]
 handleEvent' eh evt@(Ready rd@ReadyData { user, guilds }) = do
   updateCache evt
   pure $ map ($ rd) (unwrapEvent @"ready" eh)
@@ -333,7 +330,7 @@
 
 handleEvent' _ e = fail $ "Unhandled event: " <> show e
 
-updateCache :: P.Members '[CacheEff, P.Fail] r => DispatchData -> Sem r ()
+updateCache :: P.Members '[CacheEff, P.Fail] r => DispatchData -> P.Sem r ()
 updateCache (Ready ReadyData { user, guilds }) = do
   setBotUser user
   for_ (map getID guilds) setUnavailableGuild
diff --git a/src/Calamity/Client/ShardManager.hs b/src/Calamity/Client/ShardManager.hs
--- a/src/Calamity/Client/ShardManager.hs
+++ b/src/Calamity/Client/ShardManager.hs
@@ -17,15 +17,12 @@
 import           Fmt
 
 import           Polysemy                    ( Sem )
+import qualified Polysemy.Fail               as P
 import qualified Polysemy                    as P
 import qualified Polysemy.Reader             as P
 
-aa :: (Show a, Monad m) => Either a b -> m b
-aa (Right x) = pure x
-aa (Left x) = fail $ show x
-
 -- | Connects the bot to the gateway over n shards
-shardBot :: BotC r => Sem r ()
+shardBot :: (BotC r, P.Member P.Fail r) => Sem r ()
 shardBot = do
   numShardsVar <- P.asks numShards
   shardsVar <- P.asks shards
@@ -36,7 +33,7 @@
   token <- P.asks Calamity.Client.Types.token
   eventQueue <- P.asks eventQueue
 
-  gateway <- aa =<< invokeRequest GetGatewayBot
+  Right gateway <- invokeRequest GetGatewayBot
 
   let numShards' = gateway ^. #shards
   let host = gateway ^. #url
diff --git a/src/Calamity/Client/Types.hs b/src/Calamity/Client/Types.hs
--- a/src/Calamity/Client/Types.hs
+++ b/src/Calamity/Client/Types.hs
@@ -2,11 +2,13 @@
 module Calamity.Client.Types
     ( Client(..)
     , BotC
+    , SetupEff
     , EHType
     , EHType'
     , EventHandlers(..)
     , EventHandler(..) ) where
 
+import           Calamity.Metrics.Eff
 import           Calamity.Cache.Eff
 import           Calamity.Gateway.DispatchEvents
 import           Calamity.Gateway.Shard
@@ -47,7 +49,12 @@
   }
   deriving ( Generic )
 
-type BotC r = (LogC r, P.Members '[CacheEff, P.Reader Client, P.AtomicState EventHandlers, P.Embed IO, P.Final IO, P.Async] r, Typeable r)
+type BotC r =
+  ( P.Members '[LogEff, MetricEff, CacheEff, P.Reader Client,
+  P.AtomicState EventHandlers, P.Embed IO, P.Final IO, P.Async] r
+  , Typeable r)
+
+type SetupEff r = P.Sem (LogEff ': P.Reader Client ': P.AtomicState EventHandlers ': P.Async ': r) ()
 
 type family EHType d m where
   EHType "ready"                    m = ReadyData                                   -> m ()
diff --git a/src/Calamity/Gateway/Shard.hs b/src/Calamity/Gateway/Shard.hs
--- a/src/Calamity/Gateway/Shard.hs
+++ b/src/Calamity/Gateway/Shard.hs
@@ -10,6 +10,7 @@
 import           Calamity.Gateway.Types
 import           Calamity.Internal.Utils
 import           Calamity.LogEff
+import           Calamity.Metrics.Eff
 import           Calamity.Types.Token
 
 import           Control.Concurrent
@@ -72,7 +73,7 @@
 newShardState shard = ShardState shard Nothing Nothing False Nothing Nothing Nothing
 
 -- | Creates and launches a shard
-newShard :: P.Members '[LogEff, P.Embed IO, P.Final IO, P.Async] r
+newShard :: P.Members '[LogEff, MetricEff, P.Embed IO, P.Final IO, P.Async] r
          => Text
          -> Int
          -> Int
@@ -141,7 +142,7 @@
         r <- atomically $ tryWriteTBMQueue' outqueue (Control v)
         when r inner
 
-  discordStream :: P.Members '[LogEff, P.Embed IO] r => Connection -> TBMQueue ShardMsg -> Sem r ()
+  discordStream :: P.Members '[LogEff, MetricEff, P.Embed IO] r => Connection -> TBMQueue ShardMsg -> Sem r ()
   discordStream ws outqueue = inner
     where inner = do
             msg <- P.embed . checkWSClose $ receiveData ws
diff --git a/src/Calamity/Gateway/Types.hs b/src/Calamity/Gateway/Types.hs
--- a/src/Calamity/Gateway/Types.hs
+++ b/src/Calamity/Gateway/Types.hs
@@ -1,11 +1,10 @@
-{-# OPTIONS_GHC -Wno-orphans #-}
-
 -- | Types for shards
 module Calamity.Gateway.Types where
 
 import           Calamity.Gateway.DispatchEvents
 import           Calamity.Internal.AesonThings
 import           Calamity.LogEff
+import           Calamity.Metrics.Eff
 import           Calamity.Types.Model.Guild.Guild
 import           Calamity.Types.Model.Voice
 import           Calamity.Types.Snowflake
@@ -14,7 +13,6 @@
 import           Control.Concurrent.STM.TQueue
 import           Control.Concurrent.STM.TVar
 import           Control.Exception
-import           Control.Monad                    ( fail )
 
 import           Data.Aeson
 import qualified Data.Aeson.Types                 as AT
@@ -30,6 +28,9 @@
 import qualified Polysemy.Async                   as P
 import qualified Polysemy.AtomicState             as P
 
+type ShardC r = P.Members '[LogEff, P.AtomicState ShardState, P.Embed IO, P.Final IO,
+  P.Async, MetricEff] r
+
 data ShardMsg
   = Discord ReceivedDiscordMessage
   | Control ControlMessage
@@ -243,5 +244,3 @@
   , wsConn     :: Maybe Connection
   }
   deriving ( Generic )
-
-type ShardC r = (LogC r, P.Members '[P.AtomicState ShardState, P.Embed IO, P.Final IO, P.Async] r)
diff --git a/src/Calamity/HTTP/Internal/Ratelimit.hs b/src/Calamity/HTTP/Internal/Ratelimit.hs
--- a/src/Calamity/HTTP/Internal/Ratelimit.hs
+++ b/src/Calamity/HTTP/Internal/Ratelimit.hs
@@ -54,14 +54,8 @@
 getRateLimit :: RateLimitState -> Route -> STM Lock
 getRateLimit s h = SC.focus (lookupOrInsertDefaultM L.new) h (rateLimits s)
 
-doDiscordRequest
-  :: BotC r
-  => IO (Response LB.ByteString)
-  -> Sem r DiscordResponseType
+doDiscordRequest :: BotC r => IO (Response LB.ByteString) -> Sem r DiscordResponseType
 doDiscordRequest r = do
-  --debug "making request"
-  --maskState <- P.embed getMaskingState
-  --print maskState
   r' <- P.embed r
   let status = r' ^. responseStatus
   if
@@ -69,11 +63,8 @@
       let resp = r' ^. responseBody
       debug $ "Got good response from discord: " +|| r' ^. responseStatus ||+ ""
       pure $ if isExhausted r'
-        then ExhaustedBucket resp $ parseRateLimitHeader r'
-        else Good resp
-    | statusIsServerError status -> do
-      debug $ "Got server error from discord: " +| status ^. statusCode |+ ""
-      pure $ ServerError (status ^. statusCode)
+             then ExhaustedBucket resp $ parseRateLimitHeader r'
+             else Good resp
     | status == status429 -> do
       debug "Got 429 from discord, retrying."
       case asValue r' of
@@ -83,7 +74,9 @@
       let err = r' ^. responseBody
       error $ "You fucked up: " +|| err ||+ " response: " +|| r' ||+ ""
       pure $ ClientError (status ^. statusCode) err
-    | otherwise -> fail "Bogus response, discord fix your shit"
+    | otherwise -> do
+      debug $ "Got server error from discord: " +| status ^. statusCode |+ ""
+      pure $ ServerError (status ^. statusCode)
 
 parseDiscordTime :: ByteString -> Maybe UTCTime
 parseDiscordTime s = httpDateToUTC <$> parseHTTPDate s
diff --git a/src/Calamity/Internal/Utils.hs b/src/Calamity/Internal/Utils.hs
--- a/src/Calamity/Internal/Utils.hs
+++ b/src/Calamity/Internal/Utils.hs
@@ -1,3 +1,5 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+-- | Internal utilities and instances
 module Calamity.Internal.Utils
     ( whenJust
     , whenM
@@ -11,7 +13,9 @@
 
 import           Calamity.LogEff
 
+import           Data.Default.Class
 import qualified Data.HashMap.Lazy   as LH
+import qualified Data.Map            as M
 import           Data.Semigroup      ( Last(..) )
 import           Data.Text.Lazy
 import           Data.Time
@@ -61,6 +65,12 @@
 instance (TextShow a, VU.Unbox a) => TextShow (Vector a) where
   showb = showbList . VU.toList
 
--- lazy and strict use the same internal data structure
+   -- lazy and strict use the same internal data structure
 instance (Show k, Show v) => TextShow (LH.HashMap k v) where
   showb = fromString . show
+
+instance (Show k, Show v) => TextShow (M.Map k v) where
+  showb = fromString . show
+
+instance Default (M.Map k v) where
+  def = M.empty
diff --git a/src/Calamity/Metrics/Eff.hs b/src/Calamity/Metrics/Eff.hs
new file mode 100644
--- /dev/null
+++ b/src/Calamity/Metrics/Eff.hs
@@ -0,0 +1,62 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | Effect for handling metrics
+module Calamity.Metrics.Eff
+    ( Counter
+    , Gauge
+    , Histogram
+    , HistogramSample
+    , MetricEff(..)
+    , registerCounter
+    , registerGauge
+    , registerHistogram
+    , addCounter
+    , modifyGauge
+    , observeHistogram ) where
+
+import           Calamity.Internal.Utils   ()
+import           Calamity.Metrics.Internal
+
+import           Data.Default.Class
+import           Data.Map
+import           Data.Text
+
+import           GHC.Generics
+
+import           Polysemy
+
+import           TextShow
+import qualified TextShow.Generic          as TSG
+
+data HistogramSample = HistogramSample
+  { buckets :: Map Double Double
+  , sum     :: Double
+  , count   :: Int
+  }
+  deriving ( Eq, Show, Generic, Default )
+  deriving ( TextShow ) via TSG.FromGeneric HistogramSample
+
+data MetricEff m a where
+  -- | Register a 'Counter'
+  RegisterCounter :: Text -- ^ Name
+    -> [(Text, Text)] -- ^ Labels
+    -> MetricEff m Counter
+
+  -- | Register a 'Gauge'
+  RegisterGauge :: Text -- ^ Name
+    -> [(Text, Text)] -- ^ Labels
+    -> MetricEff m Gauge
+
+  -- | Register a 'Histogram'
+  RegisterHistogram :: Text -- ^ Name
+    -> [(Text, Text)] -- ^ Labels
+    -> [Double] -- ^ Upper bounds
+    -> MetricEff m Histogram
+
+  AddCounter :: Int -> Counter -> MetricEff m Int
+
+  ModifyGauge :: (Double -> Double) -> Gauge -> MetricEff m Double
+
+  ObserveHistogram :: Double -> Histogram -> MetricEff m HistogramSample
+
+makeSem ''MetricEff
diff --git a/src/Calamity/Metrics/Internal.hs b/src/Calamity/Metrics/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Calamity/Metrics/Internal.hs
@@ -0,0 +1,20 @@
+-- | Internal data structures to the metrics effect
+module Calamity.Metrics.Internal
+    ( Counter(..)
+    , Gauge(..)
+    , Histogram(..) ) where
+
+-- | A handle to a counter
+newtype Counter = Counter
+  { unCounter :: Int
+  }
+
+-- | A handle to a gauge
+newtype Gauge = Gauge
+  { unGauge :: Int
+  }
+
+-- | A handle to a histogram
+newtype Histogram = Histogram
+  { unHistogram :: Int
+  }
diff --git a/src/Calamity/Metrics/Noop.hs b/src/Calamity/Metrics/Noop.hs
new file mode 100644
--- /dev/null
+++ b/src/Calamity/Metrics/Noop.hs
@@ -0,0 +1,20 @@
+-- | Noop handler for metrics
+module Calamity.Metrics.Noop
+    ( runMetricsNoop ) where
+
+import           Calamity.Metrics.Eff
+import           Calamity.Metrics.Internal
+
+import           Data.Default.Class
+
+import           Polysemy
+
+runMetricsNoop :: Sem (MetricEff ': r) a -> Sem r a
+runMetricsNoop = interpret $ \case
+  RegisterCounter _ _     -> pure (Counter 0)
+  RegisterGauge _ _       -> pure (Gauge 0)
+  RegisterHistogram _ _ _ -> pure (Histogram 0)
+
+  AddCounter _ _          -> pure def
+  ModifyGauge _ _         -> pure def
+  ObserveHistogram _ _    -> pure def
