diff --git a/examples/cdc_rx_thruput.grc b/examples/cdc_rx_thruput.grc
new file mode 100644
index 0000000000000000000000000000000000000000..4f640fd5ee0720b89d3b758c67123237b1929a3c
--- /dev/null
+++ b/examples/cdc_rx_thruput.grc
@@ -0,0 +1,135 @@
+options:
+  parameters:
+    author: ''
+    catch_exceptions: 'True'
+    category: '[GRC Hier Blocks]'
+    cmake_opt: ''
+    comment: ''
+    copyright: University of Melbourne
+    description: ''
+    gen_cmake: 'On'
+    gen_linking: dynamic
+    generate_options: qt_gui
+    hier_block_src_path: '.:'
+    id: cdc_rx_thruput
+    max_nouts: '0'
+    output_language: python
+    placement: (0,0)
+    qt_qss_theme: ''
+    realtime_scheduling: ''
+    run: 'True'
+    run_command: '{python} -u {filename}'
+    run_options: prompt
+    sizing_mode: fixed
+    thread_safe_setters: ''
+    title: CDC Rx Thruput Example
+    window_size: (1000,1000)
+  states:
+    bus_sink: false
+    bus_source: false
+    bus_structure: null
+    coordinate: [8, 8]
+    rotation: 0
+    state: enabled
+
+blocks:
+- name: reset_var
+  id: variable_qtgui_push_button
+  parameters:
+    comment: ''
+    gui_hint: ''
+    label: Reset Stats
+    pressed: 'True'
+    released: 'False'
+    type: bool
+    value: '0'
+  states:
+    bus_sink: false
+    bus_source: false
+    bus_structure: null
+    coordinate: [608, 20.0]
+    rotation: 0
+    state: true
+- name: blocks_message_strobe_0
+  id: blocks_message_strobe
+  parameters:
+    affinity: ''
+    alias: ''
+    comment: ''
+    maxoutbuf: '0'
+    minoutbuf: '0'
+    msg: pmt.intern("TEST")
+    period: '200'
+  states:
+    bus_sink: false
+    bus_source: false
+    bus_structure: null
+    coordinate: [80, 132.0]
+    rotation: 0
+    state: true
+- name: blocks_random_pdu_0
+  id: blocks_random_pdu
+  parameters:
+    affinity: ''
+    alias: ''
+    comment: ''
+    length_modulo: '2'
+    mask: '0xFF'
+    maxoutbuf: '0'
+    maxsize: '128'
+    minoutbuf: '0'
+    minsize: '128'
+  states:
+    bus_sink: false
+    bus_source: false
+    bus_structure: null
+    coordinate: [336, 116.0]
+    rotation: 0
+    state: true
+- name: elen90089_rx_stats_0
+  id: elen90089_rx_stats
+  parameters:
+    affinity: ''
+    alias: ''
+    comment: ''
+    maxoutbuf: '0'
+    minoutbuf: '0'
+    period: '1'
+    reset: reset_var
+  states:
+    bus_sink: false
+    bus_source: false
+    bus_structure: null
+    coordinate: [624, 132.0]
+    rotation: 0
+    state: true
+- name: qtgui_edit_box_msg_0
+  id: qtgui_edit_box_msg
+  parameters:
+    affinity: ''
+    alias: ''
+    comment: ''
+    gui_hint: ''
+    is_pair: 'False'
+    is_static: 'False'
+    key: ''
+    label: Thruput (kbps)
+    maxoutbuf: '0'
+    minoutbuf: '0'
+    type: double
+    value: '0.0'
+  states:
+    bus_sink: false
+    bus_source: false
+    bus_structure: null
+    coordinate: [832, 108.0]
+    rotation: 0
+    state: true
+
+connections:
+- [blocks_message_strobe_0, strobe, blocks_random_pdu_0, generate]
+- [blocks_random_pdu_0, pdus, elen90089_rx_stats_0, pdu]
+- [elen90089_rx_stats_0, tput, qtgui_edit_box_msg_0, val]
+
+metadata:
+  file_format: 1
diff --git a/grc/CMakeLists.txt b/grc/CMakeLists.txt
index 6cc623ecfbecc523cfc77528df78c1c0db044f21..5fa604dde61c7b509fdb2616369139825f17eb26 100644
--- a/grc/CMakeLists.txt
+++ b/grc/CMakeLists.txt
@@ -15,5 +15,6 @@ install(FILES
     elen90089_constellation_decoder_cf.block.yml
     elen90089_costas_loop_cc.block.yml
     elen90089_dsa_pu_scenario_cc.block.yml
+    elen90089_rx_stats.block.yml
     DESTINATION share/gnuradio/grc/blocks
 )
diff --git a/grc/elen90089_rx_stats.block.yml b/grc/elen90089_rx_stats.block.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cbf5222b67aefb0c2c795c48c01156f3b2e205a0
--- /dev/null
+++ b/grc/elen90089_rx_stats.block.yml
@@ -0,0 +1,31 @@
+id: elen90089_rx_stats
+label: CDC Rx Stats
+category: '[elen90089]'
+
+templates:
+  imports: import elen90089
+  make: elen90089.rx_stats(${period})
+  callbacks:
+    - reset_stats(${reset})
+
+parameters:
+- id: period
+  label: Display Period (sec)
+  dtype: int
+  default: 1
+- id: reset
+  label: Reset Stats
+  dtype: raw
+  default: False
+
+inputs:
+- label: pdu
+  domain: message
+  optional: True
+
+outputs:
+- label: tput
+  domain: message
+  optional: True
+
+file_format: 1
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index 469d521dd6ab0ae9c91622dfaae3ecc1c3e4bcb1..174a0d209cf6bfdab3d107ae43630748a0b87324 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -23,6 +23,7 @@ GR_PYTHON_INSTALL(
     FILES
     __init__.py
     packet_mac_tx.py
+    rx_stats.py
     DESTINATION ${GR_PYTHON_DIR}/elen90089
 )
 
diff --git a/python/__init__.py b/python/__init__.py
index d997af05f5d3e93aeddd2bcdb23193d36d18e18f..53733ba83e97b41aee5c37d599278165eda46d17 100644
--- a/python/__init__.py
+++ b/python/__init__.py
@@ -21,4 +21,5 @@ except ModuleNotFoundError:
 
 # import any pure python here
 from .packet_mac_tx import packet_mac_tx
+from .rx_stats import rx_stats
 #
diff --git a/python/rx_stats.py b/python/rx_stats.py
new file mode 100644
index 0000000000000000000000000000000000000000..8361c421afa3d4f16c53ed3bdaf30f142c4ec58c
--- /dev/null
+++ b/python/rx_stats.py
@@ -0,0 +1,55 @@
+import pmt
+import time
+from threading import Lock
+
+import numpy as np
+from gnuradio import gr
+
+
+class rx_stats(gr.sync_block):
+    """Compute throughput (in kbps) of received PDUs."""
+    def __init__(self, period=1.0):
+        gr.sync_block.__init__(
+            self,
+            name='Rx Stats',
+            in_sig=None,
+            out_sig=None)
+        self.period = period # output period
+        
+        # message ports
+        self.message_port_register_in(pmt.intern('pdu'))
+        self.message_port_register_out(pmt.intern('tput'))
+        self.set_msg_handler(pmt.intern('pdu'), self.handle_pdu)
+        
+        # stats
+        self.mutex = Lock()
+        self.is_reset = False
+        self.n_bytes = 0
+        self.t_start = self.t_print = time.time()
+
+    def reset_stats(self, is_reset):
+        if is_reset:
+            self.mutex.acquire()
+            self.is_reset = True
+            self.mutex.release()
+
+    def handle_pdu(self, pdu):
+        # update stats
+        pld = pmt.to_python(pmt.cdr(pdu))
+        self.n_bytes += len(pld)
+        now = time.time()
+        
+        # check reset
+        self.mutex.acquire()
+        is_reset, self.is_reset = self.is_reset, False
+        self.mutex.release()
+        
+        if is_reset:
+            self.n_bytes = 0
+            self.t_start = self.t_print = now
+        
+        if now - self.t_print > self.period:
+           tput = 8*self.n_bytes / (now - self.t_start) / 1000.0
+           self.message_port_pub(pmt.intern('tput'), pmt.from_double(tput))
+           self.t_print = now
+