diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,19 @@
+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.0.0.4...main)
+
+None.
+
+## [v1.0.0.4](https://github.com/freckle/freckle-app/compare/v1.0.0.3...v1.0.0.4)
+
+- Support seconds or milliseconds in `PGSTATEMENTTIMEOUT`
+
+  NOTE: We consider this a non-breaking change because the environment variable
+  interface is backwards-compatible. By normal Haskell rules, it would be major
+  since it's changing the type of something exported.
+
+- Add `respondQueryCanceled` Yesod Middlewares
+
+- Add `makeRequestMetricsMiddleware`
+
 ## [v1.0.0.3](https://github.com/freckle/freckle-app/compare/v1.0.0.2...v1.0.0.3)
 
 - Add `package.yaml` to `extra-source-files`.
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.12
 name:               freckle-app
-version:            1.0.0.3
+version:            1.0.0.4
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
@@ -40,7 +40,6 @@
         Freckle.App.Test
         Freckle.App.Test.DocTest
         Freckle.App.Test.Hspec.Runner
-        Freckle.App.Time
         Freckle.App.Version
         Freckle.App.Wai
         Freckle.App.Yesod
diff --git a/library/Freckle/App/Database.hs b/library/Freckle/App/Database.hs
--- a/library/Freckle/App/Database.hs
+++ b/library/Freckle/App/Database.hs
@@ -10,13 +10,14 @@
   -- * Abstract over access to a sql database
     HasSqlPool(..)
   , SqlPool
-  , Seconds(..)
   , makePostgresPool
   , makePostgresPoolWith
   , runDB
   , PostgresConnectionConf(..)
   , PostgresPasswordSource(..)
   , PostgresPassword(..)
+  , PostgresStatementTimeout(..)
+  , postgresStatementTimeoutMilliseconds
   , envParseDatabaseConf
   , envPostgresPasswordSource
   ) where
@@ -30,6 +31,7 @@
 import Control.Monad.Reader
 import Data.ByteString (ByteString)
 import qualified Data.ByteString.Char8 as BS8
+import Data.Char (isDigit)
 import Data.IORef
 import Data.Pool
 import qualified Data.Text as T
@@ -39,7 +41,6 @@
   (Connection, Only(..), connectPostgreSQL, execute)
 import Database.PostgreSQL.Simple.SqlQQ (sql)
 import qualified Freckle.App.Env as Env
-import Freckle.App.Time (Seconds(..))
 import System.Process (readProcess)
 
 type SqlPool = Pool SqlBackend
@@ -71,7 +72,7 @@
   , pccPassword :: PostgresPassword
   , pccDatabase :: String
   , pccPoolSize :: Int
-  , pccStatementTimeout :: Seconds
+  , pccStatementTimeout :: PostgresStatementTimeout
   }
   deriving stock (Show, Eq)
 
@@ -85,6 +86,42 @@
   | PostgresPasswordStatic String
   deriving stock (Show, Eq)
 
+data PostgresStatementTimeout
+  = PostgresStatementTimeoutSeconds Int
+  | PostgresStatementTimeoutMilliseconds Int
+  deriving stock (Show, Eq)
+
+postgresStatementTimeoutMilliseconds :: PostgresStatementTimeout -> Int
+postgresStatementTimeoutMilliseconds = \case
+  PostgresStatementTimeoutSeconds s -> s * 1000
+  PostgresStatementTimeoutMilliseconds ms -> ms
+
+-- | Read @PGSTATEMENTTIMEOUT@ as seconds or milliseconds
+--
+-- >>> readPostgresStatementTimeout "10"
+-- Right (PostgresStatementTimeoutSeconds 10)
+--
+-- >>> readPostgresStatementTimeout "10s"
+-- Right (PostgresStatementTimeoutSeconds 10)
+--
+-- >>> readPostgresStatementTimeout "10ms"
+-- Right (PostgresStatementTimeoutMilliseconds 10)
+--
+-- >>> readPostgresStatementTimeout "20m"
+-- Left "..."
+--
+-- >>> readPostgresStatementTimeout "2m0"
+-- Left "..."
+--
+readPostgresStatementTimeout
+  :: String -> Either String PostgresStatementTimeout
+readPostgresStatementTimeout x = case span isDigit x of
+  ("", _) -> Left "must be {digits}(s|ms)"
+  (digits, "") -> Right $ PostgresStatementTimeoutSeconds $ read digits
+  (digits, "s") -> Right $ PostgresStatementTimeoutSeconds $ read digits
+  (digits, "ms") -> Right $ PostgresStatementTimeoutMilliseconds $ read digits
+  _ -> Left "must be {digits}(s|ms)"
+
 envPostgresPasswordSource :: Env.Parser PostgresPasswordSource
 envPostgresPasswordSource = do
   useIam <- Env.switch "USE_RDS_IAM_AUTH" $ Env.def False
@@ -104,7 +141,9 @@
   database <- Env.var Env.str "PGDATABASE" Env.nonEmpty
   port <- Env.var Env.auto "PGPORT" Env.nonEmpty
   poolSize <- Env.var Env.auto "PGPOOLSIZE" $ Env.def 1
-  statementTimeout <- Env.var Env.auto "PGSTATEMENTTIMEOUT" $ Env.def 120
+  statementTimeout <-
+    Env.var (Env.eitherReader readPostgresStatementTimeout) "PGSTATEMENTTIMEOUT"
+      $ Env.def (PostgresStatementTimeoutSeconds 120)
   pure PostgresConnectionConf
     { pccHost = host
     , pccPort = port
@@ -178,7 +217,7 @@
 
 setTimeout :: PostgresConnectionConf -> Connection -> IO ()
 setTimeout PostgresConnectionConf {..} conn =
-  let timeoutMillis = unSeconds pccStatementTimeout * 1000
+  let timeoutMillis = postgresStatementTimeoutMilliseconds pccStatementTimeout
   in void $ execute conn [sql| SET statement_timeout = ? |] (Only timeoutMillis)
 
 makePostgresPoolWith :: PostgresConnectionConf -> IO SqlPool
diff --git a/library/Freckle/App/Time.hs b/library/Freckle/App/Time.hs
deleted file mode 100644
--- a/library/Freckle/App/Time.hs
+++ /dev/null
@@ -1,10 +0,0 @@
--- | Time types and functions for @App@
-module Freckle.App.Time
-  ( Seconds(..)
-  ) where
-
-import Prelude
-
-newtype Seconds = Seconds { unSeconds :: Int }
-  deriving stock (Show, Read)
-  deriving newtype (Eq, Num)
diff --git a/library/Freckle/App/Wai.hs b/library/Freckle/App/Wai.hs
--- a/library/Freckle/App/Wai.hs
+++ b/library/Freckle/App/Wai.hs
@@ -1,18 +1,21 @@
+{-# LANGUAGE TupleSections #-}
+
 -- | Integration of "Freckle.App" tooling with "Network.Wai"
 module Freckle.App.Wai
   ( RouteName(..)
   , TraceId(..)
   , makeLoggingMiddleware
+  , makeRequestMetricsMiddleware
   , noCacheMiddleware
   , corsMiddleware
-  )
-where
+  ) where
 
 import Prelude
 
 import Control.Applicative ((<|>))
 import Control.Monad (guard)
 import Control.Monad.Logger (LogLevel(..))
+import Control.Monad.Reader (runReaderT)
 import Data.Aeson
 import Data.Bifunctor (first)
 import Data.ByteString (ByteString)
@@ -23,10 +26,13 @@
 import qualified Data.CaseInsensitive as CI
 import Data.Default (def)
 import Data.IP (fromHostAddress, fromHostAddress6)
-import Data.Maybe (fromMaybe)
-import Data.Text (Text)
+import Data.Maybe (fromMaybe, maybeToList)
+import Data.Text (Text, pack)
 import Data.Text.Encoding (decodeUtf8With)
 import Data.Text.Encoding.Error (lenientDecode)
+import Data.Time (getCurrentTime)
+import Freckle.App.Datadog (HasDogStatsClient, HasDogStatsTags)
+import qualified Freckle.App.Datadog as Datadog
 import Freckle.App.Logging
 import Network.HTTP.Types (QueryItem, ResponseHeaders)
 import Network.HTTP.Types.Status (Status, status200, statusCode)
@@ -158,6 +164,25 @@
 sockAddrToIp (SockAddrInet _ h) = BS8.pack $ show $ fromHostAddress h
 sockAddrToIp (SockAddrInet6 _ _ h _) = BS8.pack $ show $ fromHostAddress6 h
 sockAddrToIp (SockAddrUnix _) = "<socket>"
+
+makeRequestMetricsMiddleware
+  :: (HasDogStatsClient env, HasDogStatsTags env)
+  => env
+  -> (Request -> Maybe RouteName)
+  -> Middleware
+makeRequestMetricsMiddleware env getRouteName app req sendResponse' = do
+  start <- getCurrentTime
+  app req $ \res -> do
+    flip runReaderT env $ do
+      Datadog.increment "requests" $ tags res
+      Datadog.histogramSinceMs "response_time_ms" (tags res) start
+    sendResponse' res
+ where
+  tags res =
+    maybeToList (("route", ) . unRouteName <$> getRouteName req)
+      <> [ ("method", decodeUtf8 $ requestMethod req)
+         , ("status", pack $ show $ statusCode $ responseStatus res)
+         ]
 
 noCacheMiddleware :: Middleware
 noCacheMiddleware = addHeaders [cacheControlHeader]
diff --git a/library/Freckle/App/Yesod.hs b/library/Freckle/App/Yesod.hs
--- a/library/Freckle/App/Yesod.hs
+++ b/library/Freckle/App/Yesod.hs
@@ -2,15 +2,24 @@
 module Freckle.App.Yesod
   ( makeLogger
   , messageLoggerSource
-  )
-where
 
+  -- * Functions for use as 'yesodMiddleware'
+  , respondQueryCanceled
+  , respondQueryCanceledHeaders
+  ) where
+
 import Prelude
 
-import Control.Monad (when)
+import Control.Monad (guard, when)
 import Control.Monad.Logger
+import Data.Text (pack)
+import Database.PostgreSQL.Simple (SqlError(..))
+import Freckle.App.Datadog (HasDogStatsClient, HasDogStatsTags)
+import qualified Freckle.App.Datadog as Datadog
 import Freckle.App.GlobalCache
 import Freckle.App.Logging
+import Network.HTTP.Types (ResponseHeaders, status503)
+import qualified Network.Wai as W
 import System.IO.Unsafe (unsafePerformIO)
 import System.Log.FastLogger
   ( LoggerSet
@@ -19,7 +28,9 @@
   , newStderrLoggerSet
   , newStdoutLoggerSet
   )
-import Yesod.Core.Types (Logger, loggerPutStr)
+import UnliftIO.Exception (handleJust)
+import Yesod.Core.Handler (sendWaiResponse)
+import Yesod.Core.Types (HandlerFor, Logger, loggerPutStr)
 import Yesod.Default.Config2 (makeYesodLogger)
 
 loggerSetVar :: GlobalCache LoggerSet
@@ -53,3 +64,27 @@
         FormatJSON -> formatJsonLogStr loc src level str
         FormatTerminal ->
           formatTerminal (getLogDefaultANSI app) loc src level str
+
+-- | Catch 'SqlError' when queries are canceled due to timeout and respond 503
+--
+-- Also logs and increments a metric.
+--
+respondQueryCanceled
+  :: (HasDogStatsClient site, HasDogStatsTags site)
+  => HandlerFor site res
+  -> HandlerFor site res
+respondQueryCanceled = respondQueryCanceledHeaders []
+
+-- | 'respondQueryCanceledHeaders' but adding headers to the 503 response
+respondQueryCanceledHeaders
+  :: (HasDogStatsClient site, HasDogStatsTags site)
+  => ResponseHeaders
+  -> HandlerFor site res
+  -> HandlerFor site res
+respondQueryCanceledHeaders headers = handleJust queryCanceled $ \ex -> do
+  logErrorN $ pack $ show ex
+  Datadog.increment "query_canceled" []
+  sendWaiResponse $ W.responseLBS status503 headers "Query canceled"
+
+queryCanceled :: SqlError -> Maybe SqlError
+queryCanceled ex = ex <$ guard (sqlState ex == "57014")
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,6 +1,6 @@
 ---
 name: freckle-app
-version: 1.0.0.3
+version: 1.0.0.4
 maintainer: Freckle Education
 category: Utils
 github: freckle/freckle-app
