diff --git a/ipython-kernel.cabal b/ipython-kernel.cabal
--- a/ipython-kernel.cabal
+++ b/ipython-kernel.cabal
@@ -1,5 +1,5 @@
 name:                ipython-kernel
-version:             0.11.0.0
+version:             0.12.0.0
 synopsis:            A library for creating kernels for IPython frontends
 
 description:         ipython-kernel is a library for communicating with frontends for the interactive IPython framework. It is used extensively in IHaskell, the interactive Haskell environment.
diff --git a/src/IHaskell/IPython/EasyKernel.hs b/src/IHaskell/IPython/EasyKernel.hs
--- a/src/IHaskell/IPython/EasyKernel.hs
+++ b/src/IHaskell/IPython/EasyKernel.hs
@@ -37,6 +37,7 @@
 import qualified Data.Map as Map
 import           Data.Maybe (fromMaybe)
 import qualified Data.Text as T
+import qualified Data.Text.Encoding as Text
 
 import           IHaskell.IPython.Kernel
 import           IHaskell.IPython.Message.UUID as UUID
@@ -150,11 +151,11 @@
   zmq <- liftIO $ serveProfile prof False
   execCount <- liftIO $ newMVar 0
   forever $ do
-    req <- liftIO $ readChan (shellRequestChannel zmq)
+    (repChan, req) <- liftIO $ readChan (shellRequestChannel zmq)
     repHeader <- createReplyHeader (header req)
     when (debug config) . liftIO $ print req
     reply <- replyTo config execCount zmq req repHeader
-    liftIO $ writeChan (shellReplyChannel zmq) reply
+    liftIO $ writeChan repChan reply
 
 replyTo :: MonadIO m
         => KernelConfig m output result
@@ -256,7 +257,11 @@
   return msg
 
 dupHeader :: MonadIO m => MessageHeader -> MessageType -> m MessageHeader
-dupHeader hdr mtype =
-  do
-    uuid <- liftIO UUID.random
-    return hdr { mhMessageId = uuid, mhMsgType = mtype }
+dupHeader hdr messageType = do
+  uuid <- liftIO UUID.random
+  let sessionBytes = Text.encodeUtf8 $ T.pack $ UUID.uuidToString $ mhSessionId hdr
+      -- For IOPub message types, use session ID as identifier (topic)
+      newIdentifiers = if isIOPubMessageType messageType
+                       then [sessionBytes]
+                       else mhIdentifiers hdr
+  return hdr { mhMessageId = uuid, mhMsgType = messageType, mhIdentifiers = newIdentifiers }
diff --git a/src/IHaskell/IPython/Types.hs b/src/IHaskell/IPython/Types.hs
--- a/src/IHaskell/IPython/Types.hs
+++ b/src/IHaskell/IPython/Types.hs
@@ -30,6 +30,7 @@
     Metadata(..),
     replyType,
     showMessageType,
+    isIOPubMessageType,
 
     -- ** IPython display data message
     DisplayData(..),
@@ -244,6 +245,20 @@
 showMessageType CommCloseMessage = "comm_close"
 showMessageType HistoryRequestMessage = "history_request"
 showMessageType HistoryReplyMessage = "history_reply"
+
+isIOPubMessageType :: MessageType -> Bool
+isIOPubMessageType StatusMessage = True
+isIOPubMessageType ExecuteResultMessage = True
+isIOPubMessageType StreamMessage = True
+isIOPubMessageType DisplayDataMessage = True
+isIOPubMessageType UpdateDisplayDataMessage = True
+isIOPubMessageType ExecuteInputMessage = True
+isIOPubMessageType ExecuteErrorMessage = True
+isIOPubMessageType ClearOutputMessage = True
+isIOPubMessageType CommOpenMessage = True
+isIOPubMessageType CommDataMessage = True
+isIOPubMessageType CommCloseMessage = True
+isIOPubMessageType _ = False
 
 instance FromJSON MessageType where
   parseJSON (String s) =
diff --git a/src/IHaskell/IPython/ZeroMQ.hs b/src/IHaskell/IPython/ZeroMQ.hs
--- a/src/IHaskell/IPython/ZeroMQ.hs
+++ b/src/IHaskell/IPython/ZeroMQ.hs
@@ -42,12 +42,16 @@
        Channels
          {
          -- | A channel populated with requests from the frontend.
-         shellRequestChannel :: Chan Message
+         -- Shared between the shell and control channels.
+         -- The channel content is a tuple (replyChannel, message):
+         --   * If the message comes from the shell channel, 'replyChannel' is the shell reply channel.
+         --   * If the message comes from the control channel, 'replyChannel' is the control reply channel.
+         shellRequestChannel :: Chan (Chan Message, Message)
          -- | Writing to this channel causes a reply to be sent to the frontend.
          , shellReplyChannel :: Chan Message
          -- | This channel is a duplicate of the shell request channel, though using a different backend
          -- socket.
-         , controlRequestChannel :: Chan Message
+         , controlRequestChannel :: Chan (Chan Message, Message)
          -- | This channel is a duplicate of the shell reply channel, though using a different backend
          -- socket.
          , controlReplyChannel :: Chan Message
@@ -69,7 +73,7 @@
   shellReqChan <- newChan
   shellRepChan <- newChan
   controlReqChan <- dupChan shellReqChan
-  controlRepChan <- dupChan shellRepChan
+  controlRepChan <- newChan
   iopubChan <- newChan
   return $! Channels
     { shellRequestChannel = shellReqChan
@@ -215,7 +219,8 @@
 shell :: Bool -> ZeroMQInterface -> Socket Router -> IO ()
 shell debug channels sock = do
   -- Receive a message and write it to the interface channel.
-  receiveMessage debug sock >>= writeChan requestChannel
+  message <- receiveMessage debug sock
+  writeChan requestChannel (replyChannel, message)
 
   -- Read the reply from the interface channel and send it.
   readChan replyChannel >>= sendMessage debug (hmacKey channels) sock
@@ -230,7 +235,8 @@
 control :: Bool -> ZeroMQInterface -> Socket Router -> IO ()
 control debug channels sock = do
   -- Receive a message and write it to the interface channel.
-  receiveMessage debug sock >>= writeChan requestChannel
+  message <- receiveMessage debug sock
+  writeChan requestChannel (replyChannel, message)
 
   -- Read the reply from the interface channel and send it.
   readChan replyChannel >>= sendMessage debug (hmacKey channels) sock
