diff --git a/.github/workflows/windows-build-and-test-compatibility.yml b/.github/workflows/windows-build-and-test-compatibility.yml
index a62a0efb..a2e63c36 100644
--- a/.github/workflows/windows-build-and-test-compatibility.yml
+++ b/.github/workflows/windows-build-and-test-compatibility.yml
@@ -10,7 +10,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        node-version: [14.21.2, 16.19.0, 18.14.2, 19.X]
+        node-version: [14.21.2, 16.19.0, 18.14.1, 19.X]
         ros_distribution:
           - foxy
           - humble
diff --git a/docs/EFFICIENCY.md b/docs/EFFICIENCY.md
index b69f6561..4c8095f1 100644
--- a/docs/EFFICIENCY.md
+++ b/docs/EFFICIENCY.md
@@ -1,7 +1,9 @@
 # Tips for efficent use of rclnodejs
-While our benchmarks place rclnodejs performance at or above that of [rclpy](https://github.com/ros2/rclpy) we recommend appyling efficient coding and configuration practices where applicable. 
+
+While our benchmarks place rclnodejs performance at or above that of [rclpy](https://github.com/ros2/rclpy) we recommend appyling efficient coding and configuration practices where applicable.
 
 ## Tip-1: Disable Parameter Services
+
 The typical ROS 2 node creation process includes creating an internal parameter service who's job is to fulfill requests for parameter meta-data and to set and update node parameters. If your ROS 2 node does not support public parameters then you can save the resources consumed by the parameter service. Disable the node parameter service by setting the `NodeOption.startParameterServices` property to false as shown below:
 
 ```
@@ -13,16 +15,54 @@ let node = new Node(nodeName, namespace, Context.defaultContext(), options);
 ```
 
 ## Tip-2: Disable LifecycleNode Lifecycle Services
+
 The LifecycleNode constructor creates 5 life-cycle services to support the ROS 2 lifecycle specification. If your LifecycleNode instance will not be operating in a managed-node context consider disabling the lifecycle services via the LifecycleNode constructor as shown:
 
 ```
 let enableLifecycleCommInterface = false;
 
 let node = new LifecycleNode(
-  nodeName, 
+  nodeName,
   namespace,
-  Context.defaultContext, 
+  Context.defaultContext,
   NodeOptions.defaultOptions,
-  enableLifecycleCommInterface 
+  enableLifecycleCommInterface
 );
 ```
+
+## Tip-3: Use Content-filtering Subscriptions
+
+The ROS Humble release introduced content-filtering topics
+which enable a subscription to limit the messages it receives
+to a subset of interest. While the application of the a content-filter
+is specific to the DDS/RMW vendor, the general approach is to apply
+filtering on the publisher side. This can reduce network bandwidth
+for pub-sub communications and message processing and memory
+overhead of rclnodejs nodes.
+
+Note: Be sure to confirm that your RMW implementation supports
+content-filter before attempting to use it. In cases where content-filtering
+is not supported your Subscription will simply ignore your filter and
+continue operating with no filtering.
+
+Example:
+
+```
+  // create a content-filter to limit incoming messages to
+  // only those with temperature > 75C.
+  const options = rclnodejs.Node.getDefaultOptions();
+  options.contentFilter = {
+    expression: 'temperature > %0',
+    parameters: [75],
+  };
+
+  node.createSubscription(
+    'sensor_msgs/msg/Temperature',
+    'temperature',
+    options,
+    (temperatureMsg) => {
+      console.log(`EMERGENCY temperature detected: ${temperatureMsg.temperature}`);
+    }
+  );
+
+```
diff --git a/example/publisher-content-filter-example.js b/example/publisher-content-filter-example.js
new file mode 100644
index 00000000..c9e07795
--- /dev/null
+++ b/example/publisher-content-filter-example.js
@@ -0,0 +1,53 @@
+// Copyright (c) 2017 Intel Corporation. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+/* eslint-disable camelcase */
+
+const rclnodejs = require('../index.js');
+
+async function main() {
+  await rclnodejs.init();
+  const node = new rclnodejs.Node('publisher_content_filter_example_node');
+  const publisher = node.createPublisher(
+    'sensor_msgs/msg/Temperature',
+    'temperature'
+  );
+
+  let count = 0;
+  setInterval(function () {
+    let temperature = (Math.random() * 100).toFixed(2);
+
+    publisher.publish({
+      header: {
+        stamp: {
+          sec: 123456,
+          nanosec: 789,
+        },
+        frame_id: 'main frame',
+      },
+      temperature: temperature,
+      variance: 0,
+    });
+
+    console.log(
+      `Publish temerature message-${++count}: ${temperature} degrees`
+    );
+  }, 750);
+
+  node.spin();
+}
+
+main();
diff --git a/example/subscription-content-filter-example.js b/example/subscription-content-filter-example.js
new file mode 100644
index 00000000..39326070
--- /dev/null
+++ b/example/subscription-content-filter-example.js
@@ -0,0 +1,95 @@
+// Copyright (c) 2023 Wayne Parrott. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+const { assertDefined } = require('dtslint/bin/util.js');
+const rclnodejs = require('../index.js');
+
+/**
+ * This example demonstrates the use of content-filtering
+ * topics (subscriptions) that were introduced in ROS 2 Humble.
+ * See the following resources for content-filtering in ROS:
+ * @see {@link Node#options}
+ * @see {@link Node#createSubscription}
+ * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
+ *
+ * Use publisher-content-filter-example.js to generate example messages.
+ *
+ * To see all published messages (filterd + unfiltered) run this
+ * from commandline:
+ *
+ *   ros2 topic echo temperature
+ *
+ * @return {undefined}
+ */
+async function main() {
+  await rclnodejs.init();
+  const node = new rclnodejs.Node('subscription_message_example_node');
+
+  let param = 50;
+
+  // create a content-filter to limit incoming messages to
+  // only those with temperature > paramC.
+  const options = rclnodejs.Node.getDefaultOptions();
+  options.contentFilter = {
+    expression: 'temperature > %0',
+    parameters: [param],
+  };
+
+  let count = 0;
+  let subscription;
+  try {
+    subscription = node.createSubscription(
+      'sensor_msgs/msg/Temperature',
+      'temperature',
+      options,
+      (temperatureMsg) => {
+        console.log(`Received temperature message-${++count}: 
+${temperatureMsg.temperature}C`);
+        if (count % 5 === 0) {
+          if (subscription.isContentFilteringEnabled()) {
+            console.log('Clearing filter');
+            subscription.clearContentFilter();
+          } else {
+            param += 10;
+            console.log('Update topic content-filter, temperature > ', param);
+            const contentFilter = {
+              expression: 'temperature > %0',
+              parameters: [param],
+            };
+            subscription.setContentFilter(contentFilter);
+          }
+          console.log(
+            'Content-filtering enabled: ',
+            subscription.isContentFilteringEnabled()
+          );
+        }
+      }
+    );
+
+    if (!subscription.isContentFilteringEnabled()) {
+      console.log('Content-filtering is not enabled on subscription.');
+    }
+  } catch (error) {
+    console.error('Unable to create content-filtering subscription.');
+    console.error(
+      'Please ensure your content-filter expression and parameters are well-formed.'
+    );
+  }
+
+  node.spin();
+}
+
+main();
diff --git a/index.js b/index.js
index 2dd532d6..803d5b99 100644
--- a/index.js
+++ b/index.js
@@ -15,6 +15,7 @@
 'use strict';
 
 const DistroUtils = require('./lib/distro.js');
+const RMWUtils = require('./lib/rmw.js');
 const { Clock, ROSClock } = require('./lib/clock.js');
 const ClockType = require('./lib/clock_type.js');
 const compareVersions = require('compare-versions');
@@ -136,6 +137,9 @@ let rcl = {
   /** {@link QoS} class */
   QoS: QoS,
 
+  /** {@link RMWUtils} */
+  RMWUtils: RMWUtils,
+
   /** {@link ROSClock} class */
   ROSClock: ROSClock,
 
diff --git a/lib/distro.js b/lib/distro.js
index 3122331c..d36ea946 100644
--- a/lib/distro.js
+++ b/lib/distro.js
@@ -42,7 +42,7 @@ const DistroUtils = {
    * @return {number} Return the rclnodejs distro identifier
    */
   getDistroId: function (distroName) {
-    const dname = distroName ? distroName : this.getDistroName();
+    const dname = distroName ? distroName.toLowerCase() : this.getDistroName();
 
     return DistroNameIdMap.has(dname)
       ? DistroNameIdMap.get(dname)
diff --git a/lib/node.js b/lib/node.js
index d4a6af74..890518f9 100644
--- a/lib/node.js
+++ b/lib/node.js
@@ -464,12 +464,7 @@ class Node extends rclnodejs.ShadowNode {
     }
 
     if (options === undefined) {
-      options = {
-        enableTypedArray: true,
-        isRaw: false,
-        qos: QoS.profileDefault,
-      };
-      return options;
+      return Node.getDefaultOptions();
     }
 
     if (options.enableTypedArray === undefined) {
@@ -484,6 +479,10 @@ class Node extends rclnodejs.ShadowNode {
       options = Object.assign(options, { isRaw: false });
     }
 
+    if (options.contentFilter === undefined) {
+      options = Object.assign(options, { contentFilter: undefined });
+    }
+
     return options;
   }
 
@@ -608,7 +607,7 @@ class Node extends rclnodejs.ShadowNode {
    */
 
   /**
-   * Create a Subscription.
+   * Create a Subscription with optional content-filtering.
    * @param {function|string|object} typeClass - The ROS message class,
         OR a string representing the message class, e.g. 'std_msgs/msg/String',
         OR an object representing the message class, e.g. {package: 'std_msgs', type: 'msg', name: 'String'}
@@ -617,9 +616,18 @@ class Node extends rclnodejs.ShadowNode {
    * @param {boolean} options.enableTypedArray - The topic will use TypedArray if necessary, default: true.
    * @param {QoS} options.qos - ROS Middleware "quality of service" settings for the subscription, default: QoS.profileDefault.
    * @param {boolean} options.isRaw - The topic is serialized when true, default: false.
+   * @param {object} [options.contentFilter=undefined] - The content-filter, default: undefined.
+   *  Confirm that your RMW supports content-filtered topics before use. 
+   * @param {string} options.contentFilter.expression - Specifies the criteria to select the data samples of
+   *  interest. It is similar to the WHERE part of an SQL clause.
+   * @param {string[]} [options.contentFilter.parameters=undefined] - Array of strings that give values to
+   *  the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
+   *  fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
    * @param {SubscriptionCallback} callback - The callback to be call when receiving the topic subscribed. The topic will be an instance of null-terminated Buffer when options.isRaw is true.
    * @return {Subscription} - An instance of Subscription.
+   * @throws {ERROR} - May throw an RMW error if content-filter is malformed. 
    * @see {@link SubscriptionCallback}
+   * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B}
    */
   createSubscription(typeClass, topic, options, callback) {
     if (typeof typeClass === 'string' || typeof typeClass === 'object') {
@@ -1645,4 +1653,25 @@ class Node extends rclnodejs.ShadowNode {
   }
 }
 
+/**
+ * Create an Options instance initialized with default values.
+ * @returns {Options} - The new initialized instance.
+ * @static
+ * @example
+ * {
+ *   enableTypedArray: true,
+ *   isRaw: false,
+ *   qos: QoS.profileDefault,
+ *   contentFilter: undefined,
+ * }
+ */
+Node.getDefaultOptions = function () {
+  return {
+    enableTypedArray: true,
+    isRaw: false,
+    qos: QoS.profileDefault,
+    contentFilter: undefined,
+  };
+};
+
 module.exports = Node;
diff --git a/lib/rmw.js b/lib/rmw.js
new file mode 100644
index 00000000..c34374b6
--- /dev/null
+++ b/lib/rmw.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const DistroUtils = require('./distro');
+
+const RMWNames = {
+  FASTRTPS: 'rmw_fastrtps_cpp',
+  CONNEXT: 'rmw_connext_cpp',
+  CYCLONEDDS: 'rmw_cyclonedds_cpp',
+  GURUMDDS: 'rmw_gurumdds_cpp',
+};
+
+const DefaultRosRMWNameMap = new Map();
+DefaultRosRMWNameMap.set('eloquent', RMWNames.FASTRTPS);
+DefaultRosRMWNameMap.set('foxy', RMWNames.FASTRTPS);
+DefaultRosRMWNameMap.set('galactic', RMWNames.CYCLONEDDS);
+DefaultRosRMWNameMap.set('humble', RMWNames.FASTRTPS);
+DefaultRosRMWNameMap.set('rolling', RMWNames.FASTRTPS);
+
+const RMWUtils = {
+  RMWNames: RMWNames,
+
+  getRMWName: function () {
+    return process.env.RMW_IMPLEMENTATION
+      ? process.env.RMW_IMPLEMENTATION
+      : DefaultRosRMWNameMap.get(DistroUtils.getDistroName());
+  },
+};
+
+module.exports = RMWUtils;
diff --git a/lib/subscription.js b/lib/subscription.js
index 96a7f261..1312aac5 100644
--- a/lib/subscription.js
+++ b/lib/subscription.js
@@ -19,8 +19,13 @@ const Entity = require('./entity.js');
 const debug = require('debug')('rclnodejs:subscription');
 
 /**
- * @class - Class representing a Subscription in ROS
+ * @class - Class representing a ROS 2 Subscription
  * @hideconstructor
+ * Includes support for content-filtering topics beginning with the
+ * ROS Humble release. To learn more about content-filtering
+ * @see {@link Node#options}
+ * @see {@link Node#createSubscription}
+ * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
  */
 
 class Subscription extends Entity {
@@ -42,13 +47,21 @@ class Subscription extends Entity {
 
   static createSubscription(nodeHandle, typeClass, topic, options, callback) {
     let type = typeClass.type();
+
+    // convert contentFilter.parameters to a string[]
+    if (options.contentFilter && options.contentFilter.parameters) {
+      options.contentFilter.parameters = options.contentFilter.parameters.map(
+        (param) => param.toString()
+      );
+    }
+
     let handle = rclnodejs.createSubscription(
       nodeHandle,
       type.pkgName,
       type.subFolder,
       type.interfaceName,
       topic,
-      options.qos
+      options
     );
     return new Subscription(handle, typeClass, topic, options, callback);
   }
@@ -66,6 +79,45 @@ class Subscription extends Entity {
   get isRaw() {
     return this._isRaw;
   }
+
+  /**
+   * Test if the RMW supports content-filtered topics and that this subscription
+   * has been configured with a wellformed content-filter.
+   * @returns {boolean} True if content-filtering will be applied; otherwise false.
+   */
+  isContentFilteringEnabled() {
+    return rclnodejs.isContentFilteringEnabled(this.handle);
+  }
+
+  /**
+   * If the RMW supports content-filtered topics set this subscription's content-filter.
+   * @param {object} contentFilter - The content-filter description.
+   * @param {string} contentFilter.expression - Specifies the criteria to select messages of interest.
+   *  It is similar to the WHERE part of an SQL clause. Clear the current contentFilter if
+   *  the expression is undefined or an empty string.
+   * @param {object[]}  [contentFilter.parameters=undefined] - Array of objects that give values to
+   *  the ‘parameters’ (i.e., "%n" tokens) in the filter_expression. The number of supplied parameters must
+   *  fit with the requested values in the filter_expression (i.e., the number of %n tokens). default: undefined.
+   * @returns {boolean} - True if successful; false otherwise
+   * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
+   */
+  setContentFilter(contentFilter) {
+    if (!contentFilter) return false;
+
+    return contentFilter.expression
+      ? rclnodejs.setContentFilter(this.handle, contentFilter)
+      : this.clearContentFilter();
+  }
+
+  /**
+   * Clear the current content-filter. No filtering is to be applied.
+   * @returns {boolean} - True if successful; false otherwise
+   */
+  clearContentFilter() {
+    return this.isContentFilteringEnabled()
+      ? rclnodejs.setContentFilter(this.handle, { expression: '' })
+      : true;
+  }
 }
 
 module.exports = Subscription;
diff --git a/package.json b/package.json
index 8dc56d57..b50fdacc 100644
--- a/package.json
+++ b/package.json
@@ -13,12 +13,18 @@
     "typescript"
   ],
   "scripts": {
-    "install": "node-gyp rebuild",
+    "build": "node-gyp -j 16 build",
+    "build:dev": "node-gyp -j 16 build --debug",
+    "rebuild": "npm run clean && node-gyp -j 16 rebuild",
+    "rebuild:dev": "npm run clean && node-gyp -j 16 rebuild --debug",
+    "generate-messages": "node scripts/generate_messages.js",
+    "clean": "node-gyp clean && rimraf ./generated",
+    "install": "npm run rebuild",
+    "postinstall": "npm run generate-messages",
     "docs": "cd docs && make",
     "test": "node --expose-gc ./scripts/run_test.js && npm run dtslint",
     "dtslint": "node scripts/generate_tsd.js && dtslint test/types",
     "lint": "eslint --max-warnings=0 --ext js,ts index.js types scripts lib example rosidl_gen rosidl_parser test benchmark/rclnodejs && node ./scripts/cpplint.js",
-    "postinstall": "node scripts/generate_messages.js",
     "format": "clang-format -i -style=file ./src/*.cpp ./src/*.hpp && prettier --write \"{lib,rosidl_gen,rostsd_gen,rosidl_parser,types,example,test,scripts,benchmark}/**/*.{js,md,ts}\" ./*.{js,md,ts}"
   },
   "bin": {
diff --git a/src/rcl_bindings.cpp b/src/rcl_bindings.cpp
index 18b58421..968211f2 100644
--- a/src/rcl_bindings.cpp
+++ b/src/rcl_bindings.cpp
@@ -652,6 +652,8 @@ NAN_METHOD(CreateSubscription) {
       *Nan::Utf8String(info[3]->ToString(currentContent).ToLocalChecked()));
   std::string topic(
       *Nan::Utf8String(info[4]->ToString(currentContent).ToLocalChecked()));
+  v8::Local<v8::Object> options =
+      info[5]->ToObject(currentContent).ToLocalChecked();
 
   rcl_subscription_t* subscription =
       reinterpret_cast<rcl_subscription_t*>(malloc(sizeof(rcl_subscription_t)));
@@ -659,12 +661,68 @@ NAN_METHOD(CreateSubscription) {
 
   rcl_subscription_options_t subscription_ops =
       rcl_subscription_get_default_options();
-  auto qos_profile = GetQoSProfile(info[5]);
 
+  v8::Local<v8::Value> qos =
+      Nan::Get(options, Nan::New("qos").ToLocalChecked()).ToLocalChecked();
+  auto qos_profile = GetQoSProfile(qos);
   if (qos_profile) {
     subscription_ops.qos = *qos_profile;
   }
 
+#if ROS_VERSION >= 2205  // 2205 => Humble+
+  if (Nan::Has(options, Nan::New("contentFilter").ToLocalChecked())
+          .FromMaybe(false)) {
+    // configure content-filter
+    v8::MaybeLocal<v8::Value> contentFilterVal =
+        Nan::Get(options, Nan::New("contentFilter").ToLocalChecked());
+
+    if (!Nan::Equals(contentFilterVal.ToLocalChecked(), Nan::Undefined())
+             .ToChecked()) {
+      v8::Local<v8::Object> contentFilter = contentFilterVal.ToLocalChecked()
+                                                ->ToObject(currentContent)
+                                                .ToLocalChecked();
+
+      // expression property is required
+      std::string expression(*Nan::Utf8String(
+          Nan::Get(contentFilter, Nan::New("expression").ToLocalChecked())
+              .ToLocalChecked()
+              ->ToString(currentContent)
+              .ToLocalChecked()));
+
+      // parameters property (string[]) is optional
+      int argc = 0;
+      char** argv = nullptr;
+
+      if (Nan::Has(contentFilter, Nan::New("parameters").ToLocalChecked())
+              .FromMaybe(false)) {
+        v8::Local<v8::Array> jsArgv = v8::Local<v8::Array>::Cast(
+            Nan::Get(contentFilter, Nan::New("parameters").ToLocalChecked())
+                .ToLocalChecked());
+        argc = jsArgv->Length();
+        if (argc > 0) {
+          argv = reinterpret_cast<char**>(malloc(argc * sizeof(char*)));
+          for (int i = 0; i < argc; i++) {
+            Nan::MaybeLocal<v8::Value> jsElement = Nan::Get(jsArgv, i);
+            Nan::Utf8String utf8_arg(jsElement.ToLocalChecked());
+            int len = utf8_arg.length() + 1;
+            argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char*)));
+            snprintf(argv[i], len, "%s", *utf8_arg);
+          }
+        }
+      }
+
+      rcl_ret_t ret = rcl_subscription_options_set_content_filter_options(
+          expression.c_str(), argc, (const char**)argv, &subscription_ops);
+
+      if (ret != RCL_RET_OK) {
+        Nan::ThrowError(rcl_get_error_string().str);
+        rcl_reset_error();
+      }
+    }
+  }
+
+#endif
+
   const rosidl_message_type_support_t* ts =
       GetMessageTypeSupport(package_name, message_sub_folder, message_name);
 
@@ -689,6 +747,83 @@ NAN_METHOD(CreateSubscription) {
   }
 }
 
+NAN_METHOD(IsContentFilteringEnabled) {
+#if ROS_VERSION < 2205  // 2205 => Humble+
+  info.GetReturnValue().Set(Nan::False());
+  return;
+#else
+
+  RclHandle* subscription_handle = RclHandle::Unwrap<RclHandle>(
+      Nan::To<v8::Object>(info[0]).ToLocalChecked());
+  rcl_subscription_t* subscription =
+      reinterpret_cast<rcl_subscription_t*>(subscription_handle->ptr());
+
+  bool is_valid = rcl_subscription_is_cft_enabled(subscription);
+  info.GetReturnValue().Set(Nan::New(is_valid));
+#endif
+}
+
+NAN_METHOD(SetContentFilter) {
+#if ROS_VERSION < 2205  // 2205 => Humble+
+  info.GetReturnValue().Set(Nan::False());
+  return;
+#else
+  v8::Local<v8::Context> currentContent = Nan::GetCurrentContext();
+  RclHandle* subscription_handle = RclHandle::Unwrap<RclHandle>(
+      Nan::To<v8::Object>(info[0]).ToLocalChecked());
+  rcl_subscription_t* subscription =
+      reinterpret_cast<rcl_subscription_t*>(subscription_handle->ptr());
+
+  v8::Local<v8::Object> contentFilter =
+      info[1]->ToObject(currentContent).ToLocalChecked();
+
+  // expression property is required
+  std::string expression(*Nan::Utf8String(
+      Nan::Get(contentFilter, Nan::New("expression").ToLocalChecked())
+          .ToLocalChecked()
+          ->ToString(currentContent)
+          .ToLocalChecked()));
+
+  // parameters property (string[]) is optional
+  int argc = 0;
+  char** argv = nullptr;
+
+  if (Nan::Has(contentFilter, Nan::New("parameters").ToLocalChecked())
+          .FromMaybe(false)) {
+    v8::Local<v8::Array> jsArgv = v8::Local<v8::Array>::Cast(
+        Nan::Get(contentFilter, Nan::New("parameters").ToLocalChecked())
+            .ToLocalChecked());
+    argc = jsArgv->Length();
+    if (argc > 0) {
+      argv = reinterpret_cast<char**>(malloc(argc * sizeof(char*)));
+      for (int i = 0; i < argc; i++) {
+        Nan::MaybeLocal<v8::Value> jsElement = Nan::Get(jsArgv, i);
+        Nan::Utf8String utf8_arg(jsElement.ToLocalChecked());
+        int len = utf8_arg.length() + 1;
+        argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char*)));
+        snprintf(argv[i], len, "%s", *utf8_arg);
+      }
+    }
+  }
+
+  // create ctf options
+  rcl_subscription_content_filter_options_t options =
+      rcl_get_zero_initialized_subscription_content_filter_options();
+
+  THROW_ERROR_IF_NOT_EQUAL(
+      RCL_RET_OK,
+      rcl_subscription_content_filter_options_init(
+          subscription, expression.c_str(), argc, (const char**)argv, &options),
+      rcl_get_error_string().str);
+
+  THROW_ERROR_IF_NOT_EQUAL(
+      RCL_RET_OK, rcl_subscription_set_content_filter(subscription, &options),
+      rcl_get_error_string().str);
+
+  info.GetReturnValue().Set(Nan::True());
+#endif
+}
+
 NAN_METHOD(CreatePublisher) {
   v8::Local<v8::Context> currentContent = Nan::GetCurrentContext();
   // Extract arguments
@@ -1782,6 +1917,8 @@ std::vector<BindingMethod> binding_methods = {
     {"getRosTimeOverrideIsEnabled", GetRosTimeOverrideIsEnabled},
     {"rclTake", RclTake},
     {"createSubscription", CreateSubscription},
+    {"isContentFilteringEnabled", IsContentFilteringEnabled},
+    {"setContentFilter", SetContentFilter},
     {"createPublisher", CreatePublisher},
     {"publish", Publish},
     {"getTopic", GetTopic},
diff --git a/test.js b/test.js
new file mode 100644
index 00000000..d64f9b96
--- /dev/null
+++ b/test.js
@@ -0,0 +1,62 @@
+// Copyright (c) 2023 Wayne Parrott. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+const { assertDefined } = require('dtslint/bin/util.js');
+const rclnodejs = require('./index.js');
+
+/**
+ * This example demonstrates the use of content-filtering
+ * topics (subscriptions) that were introduced in ROS 2 Humble.
+ * See the following resources for content-filtering in ROS:
+ * @see {@link Node#options}
+ * @see {@link Node#createSubscription}
+ * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
+ *
+ * Use publisher-content-filter-example.js to generate example messages.
+ *
+ * To see all published messages (filterd + unfiltered) run this
+ * from commandline:
+ *
+ *   ros2 topic echo temperature
+ *
+ * @return {undefined}
+ */
+async function main() {
+  await rclnodejs.init();
+  const node = new rclnodejs.Node('subscription_message_example_node');
+
+  let param = 50;
+
+  // create a content-filter to limit incoming messages to
+  // only those with temperature > paramC.
+  const options = rclnodejs.Node.getDefaultOptions();
+  options.contentFilter = {
+    expression: "data = 'foo'",
+  };
+
+  let subscription = node.createSubscription(
+    'std_msgs/msg/String',
+    'temperature',
+    options,
+    (msg) => {}
+  );
+
+  console.log('filtering enabled: ', subscription.isContentFilteringEnabled());
+
+  rclnodejs.shutdown();
+}
+
+main();
diff --git a/test/blocklist.json b/test/blocklist.json
index 197ff941..2b29c9f5 100644
--- a/test/blocklist.json
+++ b/test/blocklist.json
@@ -1,5 +1,26 @@
 {
-  "Linux": ["test-raw-pub-sub.js", "test-multi-nodes.js", "test-msg-type-py-node.js", "test-msg-type-cpp-node.js", "test-cross-lang.js"],
-  "Darwin": ["test-raw-pub-sub.js", "test-multi-nodes.js", "test-msg-type-py-node.js", "test-msg-type-cpp-node.js", "test-cross-lang.js"],
-  "Windows_NT": ["test-raw-pub-sub.js", "test-multi-nodes.js", "test-msg-type-py-node.js", "test-msg-type-cpp-node.js", "test-cross-lang.js", "test-generate-messages-bin.js"]
+  "Linux": [
+    "test-raw-pub-sub.js",
+    "test-multi-nodes.js",
+    "test-msg-type-py-node.js",
+    "test-msg-type-cpp-node.js",
+    "test-cross-lang.js"
+  ],
+  "Darwin": [
+    "test-raw-pub-sub.js", 
+    "test-multi-nodes.js", 
+    "test-msg-type-py-node.js",
+    "test-msg-type-cpp-node.js", 
+    "test-cross-lang.js", 
+    "test-subscription-content-filter.js"
+  ],
+  "Windows_NT": [
+    "test-raw-pub-sub.js", 
+    "test-multi-nodes.js", 
+    "test-msg-type-py-node.js", 
+    "test-msg-type-cpp-node.js", 
+    "test-cross-lang.js", 
+    "test-generate-messages-bin.js", 
+    "test-subscription-content-filter.js"
+  ]
 }
diff --git a/test/test-subscription-content-filter.js b/test/test-subscription-content-filter.js
new file mode 100644
index 00000000..a7157323
--- /dev/null
+++ b/test/test-subscription-content-filter.js
@@ -0,0 +1,412 @@
+'use strict';
+
+const childProcess = require('child_process');
+const assert = require('assert');
+const rclnodejs = require('../index.js');
+const DistroUtils = rclnodejs.DistroUtils;
+const RMWUtils = rclnodejs.RMWUtils;
+
+function isContentFilteringSupported() {
+  return (
+    DistroUtils.getDistroId() >= DistroUtils.getDistroId('humble') &&
+    RMWUtils.getRMWName() != RMWUtils.RMWNames.CYCLONEDDS
+  );
+}
+
+describe('subscription content-filtering', function () {
+  this.timeout(10 * 1000);
+
+  beforeEach(function () {
+    return rclnodejs.init();
+  });
+
+  afterEach(function () {
+    rclnodejs.shutdown();
+  });
+
+  it('isContentFilteringEnabled', function (done) {
+    let node = rclnodejs.createNode('string_subscription');
+    let msgString = 'std_msgs/msg/Int16';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: 'data = 16',
+    };
+
+    let subscription = node.createSubscription(
+      msgString,
+      'channel',
+      options,
+      (msg) => {}
+    );
+
+    assert.ok(
+      subscription.isContentFilteringEnabled() === isContentFilteringSupported()
+    );
+
+    node.destroySubscription(subscription);
+    subscription = node.createSubscription(
+      msgString,
+      'String_channel',
+      (msg) => {}
+    );
+    assert.ok(!subscription.isContentFilteringEnabled());
+
+    done();
+  });
+
+  it('no parameters', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('string_subscription');
+    let msgString = 'std_msgs/msg/String';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: "data = 'FilteredData'",
+    };
+
+    let msgCnt = 0;
+    let fail = false;
+    let subscription = node.createSubscription(
+      msgString,
+      'String_channel',
+      options,
+      (msg) => {
+        msgCnt++;
+        if (msg.data != 'FilteredData') {
+          fail = true;
+        }
+      }
+    );
+
+    assert.ok(subscription.isContentFilteringEnabled());
+
+    let publisher1 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'String',
+      "'FilteredData'",
+    ]);
+
+    let publisher2 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'String',
+      "'Data'",
+    ]);
+
+    setTimeout(() => {
+      publisher1.kill('SIGINT');
+      publisher2.kill('SIGINT');
+      assert.ok(msgCnt && !fail);
+      done();
+    }, 1000);
+
+    rclnodejs.spin(node);
+  });
+
+  it('single parameter', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('string_subscription');
+    let msgString = 'std_msgs/msg/String';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: 'data = %0',
+      parameters: ["'FilteredData'"],
+    };
+
+    let msgCnt = 0;
+    let fail = false;
+    let subscription = node.createSubscription(
+      msgString,
+      'String_channel',
+      options,
+      (msg) => {
+        msgCnt++;
+        if (msg.data != 'FilteredData') {
+          fail = true;
+        }
+      }
+    );
+
+    assert.ok(subscription.isContentFilteringEnabled());
+
+    let publisher1 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'String',
+      "'FilteredData'",
+    ]);
+
+    let publisher2 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'String',
+      "'Data'",
+    ]);
+
+    setTimeout(() => {
+      publisher1.kill('SIGINT');
+      publisher2.kill('SIGINT');
+      assert.ok(msgCnt && !fail);
+      done();
+    }, 1000);
+
+    rclnodejs.spin(node);
+  });
+
+  it('multiple parameters', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('int32_subscription');
+    let msgString = 'std_msgs/msg/Int32';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: 'data >= %0 AND data <= %1',
+      parameters: [5, 10],
+    };
+
+    let msgCnt = 0;
+    let fail = false;
+    let subscription = node.createSubscription(
+      msgString,
+      'Int32_channel',
+      options,
+      (msg) => {
+        msgCnt++;
+        if (msg.data === 0) {
+          fail = true;
+        }
+      }
+    );
+
+    assert.ok(subscription.isContentFilteringEnabled());
+
+    let publisher1 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'Int32',
+      '0',
+    ]);
+
+    let publisher2 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'Int32',
+      '7',
+    ]);
+
+    setTimeout(() => {
+      publisher1.kill('SIGINT');
+      publisher2.kill('SIGINT');
+      assert.ok(msgCnt && !fail);
+      done();
+    }, 1000);
+
+    rclnodejs.spin(node);
+  });
+
+  it('setContentFilter', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('int32_subscription');
+    let msgString = 'std_msgs/msg/Int32';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: 'data = %0',
+      parameters: [3],
+    };
+
+    let msgCnt0 = 0;
+    let msgCnt5 = 0;
+    let fail = false;
+    let subscription = node.createSubscription(
+      msgString,
+      'Int32_channel',
+      options,
+      (msg) => {
+        switch (msg.data) {
+          case 0:
+            msgCnt0++;
+            break;
+          case 5:
+            msgCnt5++;
+            break;
+          default:
+            fail = true;
+        }
+      }
+    );
+
+    assert.ok(subscription.isContentFilteringEnabled());
+
+    let publisher1 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'Int32',
+      '0',
+    ]);
+
+    let publisher2 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'Int32',
+      '5',
+    ]);
+
+    setTimeout(() => {
+      const contentFilter5 = {
+        expression: 'data = 5',
+      };
+      subscription.setContentFilter(contentFilter5);
+    }, 500);
+
+    setTimeout(() => {
+      publisher1.kill('SIGINT');
+      publisher2.kill('SIGINT');
+      assert.ok(!fail && msgCnt5 && !msgCnt0);
+      done();
+    }, 1000);
+
+    rclnodejs.spin(node);
+  });
+
+  it('clearContentFilter', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('int32_subscription');
+    let msgString = 'std_msgs/msg/Int32';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: 'data = %0',
+      parameters: [5],
+    };
+
+    let msgCnt0 = 0;
+    let msgCnt5 = 0;
+    let fail = false;
+    let subscription = node.createSubscription(
+      msgString,
+      'Int32_channel',
+      options,
+      (msg) => {
+        switch (msg.data) {
+          case 0:
+            msgCnt0++;
+            break;
+          case 5:
+            msgCnt5++;
+            break;
+          default:
+            fail = true;
+        }
+      }
+    );
+
+    assert.ok(subscription.isContentFilteringEnabled());
+
+    let publisher1 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'Int32',
+      '0',
+    ]);
+
+    let publisher2 = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'Int32',
+      '5',
+    ]);
+
+    setTimeout(() => {
+      assert.ok(msgCnt5 && !msgCnt0 && !fail);
+      subscription.clearContentFilter();
+    }, 500);
+
+    setTimeout(() => {
+      publisher1.kill('SIGINT');
+      publisher2.kill('SIGINT');
+      assert.ok(!subscription.isContentFilteringEnabled());
+      assert.ok(!fail && msgCnt5 && msgCnt0);
+      done();
+    }, 1000);
+
+    rclnodejs.spin(node);
+  });
+
+  it('multiple clearContentFilter', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('int32_subscription');
+    let msgString = 'std_msgs/msg/Int32';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: 'data = %0',
+      parameters: [5],
+    };
+
+    let subscription = node.createSubscription(
+      msgString,
+      'Int32_channel',
+      options,
+      (msg) => {}
+    );
+
+    assert.ok(subscription.isContentFilteringEnabled());
+    assert.ok(subscription.clearContentFilter());
+    assert.ok(subscription.clearContentFilter());
+    done();
+  });
+
+  it('no content-filter', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('string_subscription');
+    let msgString = 'std_msgs/msg/String';
+
+    let msgCnt = 0;
+    let subscription = node.createSubscription(
+      msgString,
+      'String_channel',
+      (msg) => {
+        msgCnt++;
+      }
+    );
+
+    assert.ok(!subscription.isContentFilteringEnabled());
+
+    let publisher = childProcess.fork(`${__dirname}/publisher_msg.js`, [
+      'String',
+      "'Data'",
+    ]);
+
+    setTimeout(() => {
+      publisher.kill('SIGINT');
+      assert.ok(msgCnt > 0);
+      done();
+    }, 1000);
+
+    rclnodejs.spin(node);
+  });
+
+  it('bad expression', function (done) {
+    if (!isContentFilteringSupported()) {
+      this.skip();
+    }
+
+    let node = rclnodejs.createNode('string_subscription');
+    let msgString = 'std_msgs/msg/String';
+    let options = rclnodejs.Node.getDefaultOptions();
+    options.contentFilter = {
+      expression: 'this will fail',
+    };
+
+    let subscription;
+    try {
+      subscription = subscription = node.createSubscription(
+        msgString,
+        'String_channel',
+        options,
+        (msg) => {}
+      );
+    } catch (e) {}
+
+    assert.ok(!subscription || !subscription.isContentFilteringEnabled());
+    done();
+  });
+});
diff --git a/test/types/main.ts b/test/types/main.ts
index 26de59c4..e9173553 100644
--- a/test/types/main.ts
+++ b/test/types/main.ts
@@ -110,6 +110,9 @@ node.countPublishers(TOPIC);
 // $ExpectType number
 node.countSubscribers(TOPIC);
 
+// $ExpectType Options<string | QoS>
+rclnodejs.Node.getDefaultOptions();
+
 // ---- LifecycleNode ----
 // $ExpectType LifecycleNode
 const lifecycleNode = rclnodejs.createLifecycleNode(LIFECYCLE_NODE_NAME);
@@ -215,9 +218,31 @@ let subscription = node.createSubscription(TYPE_CLASS, TOPIC, (msg) => {});
 // $ExpectType Subscription
 subscription = node.createSubscription(TYPE_CLASS, TOPIC, {}, (msg) => {});
 
+const contentFilter: rclnodejs.SubscriptionContentFilter = {
+  expression: 'data < %0',
+  parameters: [5],
+};
+
+// $ExpectType Subscription
+subscription = node.createSubscription(
+  TYPE_CLASS,
+  TOPIC,
+  { contentFilter },
+  (msg) => {}
+);
+
 // $ExpectType string
 subscription.topic;
 
+// $ExpectType boolean
+subscription.setContentFilter(contentFilter);
+
+// $ExpectType boolean
+subscription.clearContentFilter();
+
+// $ExpectType boolean
+subscription.isContentFilteringEnabled();
+
 // ---- Service ----
 // $ExpectType AddTwoIntsConstructor
 let service = node.createService(
diff --git a/types/node.d.ts b/types/node.d.ts
index 2cd1c547..0a9cd130 100644
--- a/types/node.d.ts
+++ b/types/node.d.ts
@@ -13,6 +13,30 @@ declare module 'rclnodejs' {
         name: string;
       };
 
+  /**
+   * A filter description similar to a SQL WHERE clause that limits
+   * the data wanted by a Subscription.
+   *
+   * The `expression` grammar is defined in the DDS 1.4 specification, Annex B.
+   * https://www.omg.org/spec/DDS/1.4/PDF
+   */
+  interface SubscriptionContentFilter {
+    /**
+     * Specifies the criteria to select the data samples of
+     * interest. It is similar to the WHERE part of an SQL clause.
+     * Must be a valid query clause.
+     */
+    readonly expression: string;
+
+    /**
+     * The values for the ‘parameters’ (i.e., "%n" tokens) in
+     * the filter_expression string. There must be a 1-1 match
+     * between values and expression parameters. The maximum
+     * number of parameters is 100.
+     */
+    readonly parameters?: Array<any>;
+  }
+
   /**
    * Configuration options when creating new Publishers, Subscribers,
    * Clients and Services.
@@ -37,16 +61,23 @@ declare module 'rclnodejs' {
      * ROS Middleware "quality of service" setting, default: QoS.profileDefault.
      */
     qos?: T;
+
+    /**
+     * An optional filter descriptions similar to a SQL WHERE clause used by a Subscription to
+     * inspect and limit messages that it accepts.
+     */
+    contentFilter?: SubscriptionContentFilter;
   }
 
   /**
-   * Default options when creating a Node, Publisher, Subscription, Client or Service
+   * Default options when creating a Publisher, Subscription, Client or Service
    *
    * ```ts
    * {
    *   enableTypedArray: true,
    *   qos: QoS.profileDefault,
-   *   isRaw: false
+   *   isRaw: false,
+   *   contentFilter: undefined
    * }
    * ```
    */
@@ -133,6 +164,21 @@ declare module 'rclnodejs' {
       options?: NodeOptions
     );
 
+    /**
+     * Create an Options instance initialized with default values.
+     * @returns {Options} - The new initialized instance.
+     * @example
+     * ```
+     * {
+     *   enableTypedArray: true,
+     *   isRaw: false,
+     *   qos: QoS.profileDefault,
+     *   contentFilter: undefined,
+     * }
+     * ```
+     */
+    static getDefaultOptions(): Options;
+
     /**
      * Get the name of the node.
      *
@@ -271,6 +317,8 @@ declare module 'rclnodejs' {
      * @param options - Configuration options, see DEFAULT_OPTIONS
      * @param callback - Called when a new message is received. The serialized message will be null-terminated.
      * @returns New instance of Subscription.
+     * @throws Error - May throw an RMW error if options.content-filter is malformed.
+     * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|Content-filter details at DDS 1.4 specification, Annex B}
      */
     createSubscription<T extends TypeClass<MessageTypeClassName>>(
       typeClass: T,
diff --git a/types/subscription.d.ts b/types/subscription.d.ts
index a8d891f5..0393f3bd 100644
--- a/types/subscription.d.ts
+++ b/types/subscription.d.ts
@@ -6,6 +6,7 @@ declare module 'rclnodejs' {
    *
    * @remarks
    * See {@link Node#createSubscription | Node.createSubscription}
+   * See {@link SubscriptionContentFilter}
    * See {@link Node#createPublisher | Node.createPublisher}
    * See {@link Publisher}
    * See {@link Subscription}
@@ -22,5 +23,32 @@ declare module 'rclnodejs' {
      * Topic to listen for messages on.
      */
     readonly topic: string;
+
+    /**
+     * Specifies if messages are in raw (binary) format
+     */
+    readonly isRaw: boolean;
+
+    /**
+     * Test if the RMW supports content-filtered topics and that this subscription
+     * has been configured with a wellformed content-filter.
+     * @returns {boolean} True if content-filtering will be applied; otherwise false.
+     */
+    isContentFilteringEnabled(): boolean;
+
+    /**
+     * Set a content-filter if the RMW supports content-filtered topics.
+     * @param contentFilter - The content-filter description to apply.
+     * @returns True if successful; false otherwise
+     * @remarks
+     * @see {@link https://www.omg.org/spec/DDS/1.4/PDF|DDS 1.4 specification, Annex B}
+     */
+    setContentFilter(filter: SubscriptionContentFilter): boolean;
+
+    /**
+     * Clear the current content-filter. No filtering is to be applied.
+     * @returns True if successful; false otherwise
+     */
+    clearContentFilter(): boolean;
   }
 }