diff --git a/hikaru.cabal b/hikaru.cabal
index 03f3b70ac44b3ea3cbfebbdc5a5d1d10a22699fb..fd4408540a51297998ba90695eebf47d13eda0b1 100644
--- a/hikaru.cabal
+++ b/hikaru.cabal
@@ -4,7 +4,7 @@ cabal-version: 1.12
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: f2e507d7c6d6469807c9edd22fe9f5628b08203e542661f8e917468fd1f856e8
+-- hash: 3bf5d3e31633e3ccf1a02683302849a23a53f7b621bf6e24759bdca47151b60a
 
 name:           hikaru
 version:        0.1.0.0
@@ -73,6 +73,8 @@ library
     , time >=1.9 && <1.10
     , wai >=3.2 && <3.3
     , wai-extra >=3.0 && <3.1
+    , wai-websockets >=3.0 && <3.1
+    , websockets >=0.12 && <0.13
   default-language: Haskell2010
 
 test-suite spec
@@ -115,4 +117,6 @@ test-suite spec
     , time >=1.9 && <1.10
     , wai >=3.2 && <3.3
     , wai-extra >=3.0 && <3.1
+    , wai-websockets >=3.0 && <3.1
+    , websockets >=0.12 && <0.13
   default-language: Haskell2010
diff --git a/lib/Hikaru/Action.hs b/lib/Hikaru/Action.hs
index 188dc3cc086ef8ed9855b35daa1b07b579acb69a..8fbf2db4a5127b00171c56f0b982b51dff7ea313 100644
--- a/lib/Hikaru/Action.hs
+++ b/lib/Hikaru/Action.hs
@@ -76,6 +76,15 @@ module Hikaru.Action
   , setResponseStream
   , setResponseRaw
 
+  -- ** WebSockets
+  , setFrameLimit
+  , setMessageLimit
+  , setResponseWS
+  , WebSocket
+  , wsSendText
+  , wsSendBinary
+  , wsReceive
+
   -- ** Errors
   , throwError
 
@@ -127,9 +136,12 @@ where
   import Network.HTTP.Types.Method
   import Network.HTTP.Types.Status
   import Network.Wai
+  import Network.Wai.Handler.WebSockets
   import System.IO.Unsafe
   import Web.Cookie
 
+  import qualified Network.WebSockets as WS
+
 
   -- |
   -- 'MonadAction' provides access to the original 'Request' and means to
@@ -203,6 +215,8 @@ where
       , aeBodyCounter  :: IORef Int64
       , aeLanguages    :: IORef [Text]
       , aeCache        :: IORef (Map.Map Text Dynamic)
+      , aeMsgLimit     :: IORef Int64
+      , aeFrameLimit   :: IORef Int64
       }
 
 
@@ -258,6 +272,8 @@ where
       -- ^ Body has been successfully parsed as a JSON.
     | BodyBytes LBS.ByteString
       -- ^ Body has been successfully read in raw.
+    | BodyWebSocket
+      -- ^ Body is being used for WebSockets communication.
 
 
   -- |
@@ -276,6 +292,8 @@ where
     aeBodyCounter <- newIORef 0
     aeLanguages   <- newIORef []
     aeCache       <- newIORef Map.empty
+    aeMsgLimit    <- newIORef (1 * 1024 * 1024)
+    aeFrameLimit  <- newIORef (1 * 1024 * 1024)
 
     return ActionEnv{..}
 
@@ -995,17 +1013,18 @@ where
 
 
   -- |
-  -- Escape the established narrative of 'Response' bodies and take over
-  -- the connection for any purpose you deep practical. Ideal for WebSockets
-  -- and such.
+  -- Create a raw response. This is useful for "upgrade" situations,
+  -- where an application requests for the server to grant it raw
+  -- network access.
   --
-  -- The secondary 'Response' is used when upgrading is not supported by
-  -- the underlying web server technology.
+  -- This function requires a backup response to be provided, for the
+  -- case where the handler in question does not support such upgrading.
   --
-  -- NOTE: Ignores both status and headers.
+  -- Ignores both status and headers set so far. You need to emit these
+  -- yourself, if needed.
   --
-  -- NOTE: Try not to read from the body before starting the raw response
-  --       or risk encountering an undefined behavior.
+  -- Try not to read from the body before starting the raw response
+  -- or risk encountering undefined behavior.
   --
   setResponseRaw :: (MonadAction m)
                  => (IO ByteString -> (ByteString -> IO ()) -> IO ())
@@ -1015,6 +1034,127 @@ where
     setActionField aeRespMaker \_st _hs -> responseRaw comm resp
 
 
+  -- WebSockets --------------------------------------------------------------
+
+
+  -- |
+  -- Set limit (in bytes) for reading the individual WebSocket frames
+  -- in order to prevent memory exhaustion.
+  --
+  -- Default limit is 1 MiB, which is too little for file transmission
+  -- and too much for simple notifications. You might even consider
+  -- lowering it down to e.g. 125 bytes for sockets that are supposed
+  -- to communicate in one way only.
+  --
+  setFrameLimit :: (MonadAction m) => Int64 -> m ()
+  setFrameLimit = setActionField aeFrameLimit
+
+
+  -- |
+  -- Set limit (in bytes) for reading the individual WebSocket messages
+  -- in order to prevent memory exhaustion.
+  --
+  -- Default limit is 1 MiB, which is too little for file transmission
+  -- and too much for simple notifications. You might even consider
+  -- lowering it down to e.g. 125 bytes for sockets that are supposed
+  -- to communicate in one way only.
+  --
+  -- Single message may or may not consist of multiple frames.
+  --
+  setMessageLimit :: (MonadAction m) => Int64 -> m ()
+  setMessageLimit = setActionField aeMsgLimit
+
+
+  -- |
+  -- Attempt to upgrade the connection to a WebSocket.
+  --
+  -- The 'WebSocket' monad can be used to communicate with the client.
+  --
+  -- Sets up an automatic keep-alive with a 30s ping interval.
+  --
+  setResponseWS :: (MonadAction m) => WebSocket () -> m ()
+  setResponseWS ws = do
+    -- First check the body situation.
+    body <- getActionField aeBody
+
+    case body of
+      BodyUnparsed -> do
+        frameLimit   <- WS.SizeLimit <$> getActionField aeFrameLimit
+        messageLimit <- WS.SizeLimit <$> getActionField aeMsgLimit
+
+        let opts = WS.defaultConnectionOptions
+                     { WS.connectionFramePayloadSizeLimit = frameLimit
+                     , WS.connectionMessageDataSizeLimit  = messageLimit
+                     }
+
+        req <- getRequest
+
+        setActionField aeBody BodyWebSocket
+        setActionField aeRespMaker \_st _hs ->
+          case websocketsApp opts app req of
+            Nothing   -> responseLBS status400 [] "WebSocket Expected"
+            Just resp -> resp
+
+      _else -> do
+        throwError InternalError "Body has already been consumed."
+
+    where
+      app :: WS.PendingConnection -> IO ()
+      app pc = do
+        void do
+          conn <- WS.acceptRequest pc
+          WS.withPingThread conn 30 (return ()) do
+            runReaderT (unWebSocket ws) conn
+
+
+  -- |
+  -- WebSocket context.
+  --
+  newtype WebSocket a
+    = WebSocket
+      { unWebSocket    :: ReaderT WS.Connection IO a
+      }
+    deriving (MonadUnliftIO, MonadIO, Monad, Applicative, Functor)
+
+
+  -- |
+  -- Send a textual message.
+  --
+  wsSendText :: (WS.WebSocketsData a) => a -> WebSocket ()
+  wsSendText payload = do
+    conn <- wsGetConn
+    liftIO $ WS.sendTextData conn payload
+
+
+  -- |
+  -- Send a binary message.
+  --
+  wsSendBinary :: (WS.WebSocketsData a) => a -> WebSocket ()
+  wsSendBinary payload = do
+    conn <- wsGetConn
+    liftIO $ WS.sendBinaryData conn payload
+
+
+  -- |
+  -- Receive a message decoded as either binary or text,
+  -- depending on the requested value type.
+  --
+  wsReceive :: (WS.WebSocketsData a) => WebSocket a
+  wsReceive = do
+    conn <- wsGetConn
+    liftIO $ WS.receiveData conn
+
+
+  -- |
+  -- Get the WebSocket connection.
+  --
+  wsGetConn :: WebSocket WS.Connection
+  wsGetConn = WebSocket ask
+
+
+  -- Errors ------------------------------------------------------------------
+
+
   -- |
   -- Same an IO exception in the form of ('RequestError', 'Text').
   --
diff --git a/lib/Hikaru/Route.hs b/lib/Hikaru/Route.hs
index e8902e18f20a7b4bfef895c7b519acc551a1f785..417d838ac0490b1d5df99b05ca9136495c2ad568 100644
--- a/lib/Hikaru/Route.hs
+++ b/lib/Hikaru/Route.hs
@@ -38,6 +38,9 @@ module Hikaru.Route
   , patch
   , delete
 
+  -- *** WebSockets
+  , websocket
+
   -- *** Request Content
   , acceptContent
   , acceptForm
@@ -58,13 +61,14 @@ module Hikaru.Route
 where
   import Relude hiding (get, put, head)
 
-  import Data.String.Conversions
   import Data.List (lookup)
+  import Data.String.Conversions
+  import Hikaru.Media
+  import Hikaru.Types
   import Network.HTTP.Types.Header
   import Network.HTTP.Types.Method (Method)
   import Network.Wai
-  import Hikaru.Media
-  import Hikaru.Types
+  import Network.Wai.Handler.WebSockets
 
 
   -- |
@@ -360,6 +364,16 @@ where
   delete = method "DELETE"
 
 
+  -- |
+  -- Match a WebSocket upgrade request.
+  --
+  websocket :: Route ()
+  websocket = score \req ->
+    if isWebSocketsReq req
+       then Suitable 1.0
+       else Unsuitable BadRequest "WebSocket Upgrade Expected"
+
+
   -- |
   -- Check that the content sent by the client is among the listed
   -- media types and fail with 'UnsupportedMediaType' if not.
diff --git a/package.yaml b/package.yaml
index 9d3a3372a98e47b4a4995fa0a7277a4405a13ec5..8485bddf4da2c9ab3665d05cc2519dcfb1609b98 100644
--- a/package.yaml
+++ b/package.yaml
@@ -44,27 +44,29 @@ default-extensions:
   - UndecidableInstances
 
 dependencies:
-  - aeson              >= 1.4  && <1.5
-  - base               >= 4.13 && <4.14
-  - binary             >= 0.8  && <0.9
-  - bytestring         >= 0.10 && <0.11
-  - case-insensitive   >= 1.2  && <1.3
-  - containers         >= 0.6  && <0.7
-  - cookie             >= 0.4  && <0.5
-  - cryptonite         >= 0.26 && <0.27
-  - foreign-store      >= 0.2  && <0.3
-  - http-types         >= 0.12 && <0.13
-  - lucid              >= 2.9  && <2.10
-  - memory             >= 0.15 && <0.16
-  - mtl                >= 2.2  && <2.3
-  - relude             >= 0.7  && <0.8
-  - resourcet          >= 1.2  && <1.3
-  - string-conversions >= 0.4  && <0.5
-  - text               >= 1.2  && <1.3
-  - text-icu           >= 0.7  && <0.8
-  - time               >= 1.9  && <1.10
-  - wai                >= 3.2  && <3.3
-  - wai-extra          >= 3.0  && <3.1
+  - aeson              >= 1.4   && <1.5
+  - base               >= 4.13  && <4.14
+  - binary             >= 0.8   && <0.9
+  - bytestring         >= 0.10  && <0.11
+  - case-insensitive   >= 1.2   && <1.3
+  - containers         >= 0.6   && <0.7
+  - cookie             >= 0.4   && <0.5
+  - cryptonite         >= 0.26  && <0.27
+  - foreign-store      >= 0.2   && <0.3
+  - http-types         >= 0.12  && <0.13
+  - lucid              >= 2.9   && <2.10
+  - memory             >= 0.15  && <0.16
+  - mtl                >= 2.2   && <2.3
+  - relude             >= 0.7   && <0.8
+  - resourcet          >= 1.2   && <1.3
+  - string-conversions >= 0.4   && <0.5
+  - text               >= 1.2   && <1.3
+  - text-icu           >= 0.7   && <0.8
+  - time               >= 1.9   && <1.10
+  - wai                >= 3.2   && <3.3
+  - wai-extra          >= 3.0   && <3.1
+  - wai-websockets     >= 3.0   && <3.1
+  - websockets         >= 0.12  && <0.13
 
 library:
   source-dirs: lib