diff --git a/docs/configuration/http_conn_man/route_config/route.rst b/docs/configuration/http_conn_man/route_config/route.rst
index 836e3e41f6c29..473a6d33e5ad4 100644
--- a/docs/configuration/http_conn_man/route_config/route.rst
+++ b/docs/configuration/http_conn_man/route_config/route.rst
@@ -153,7 +153,8 @@ Global rate limit :ref:`architecture overview <arch_overview_rate_limit>`.
 .. code-block:: json
 
   {
-    "global": "..."
+    "global": "...",
+    "route_key": "..."
   }
 
 global
@@ -161,6 +162,11 @@ global
   request that matches this route. This information is used by the :ref:`rate limit filter
   <config_http_filters_rate_limit>` if it is installed. Defaults to false if not specified.
 
+route_key
+  *(optional, string)* Specifies a descriptor value to be used when rate limiting for a route.
+  This information is used by the :ref:`rate limit filter
+  <config_http_filters_rate_limit>` if it is installed.
+
 .. _config_http_conn_man_route_table_route_shadow:
 
 Shadow
diff --git a/docs/configuration/http_filters/rate_limit_filter.rst b/docs/configuration/http_filters/rate_limit_filter.rst
index c8fe975a36df7..86e80fdfb7b24 100644
--- a/docs/configuration/http_filters/rate_limit_filter.rst
+++ b/docs/configuration/http_filters/rate_limit_filter.rst
@@ -40,8 +40,8 @@ Actions
   }
 
 type
-  *(required, string) The type of rate limit action to perform. The currently supported action
-  type is *service_to_service*.
+  *(required, string)* The type of rate limit action to perform. The currently supported action
+  types are *service_to_service* and *request_headers*.
 
 Service to service
 ^^^^^^^^^^^^^^^^^^
@@ -60,6 +60,33 @@ The following descriptors are sent:
 
 <local service cluster> is derived from the :option:`--service-cluster` option.
 
+Request Headers
+^^^^^^^^^^^^^^^
+
+.. code-block:: json
+
+  {
+    "type": "request_headers",
+    "header_name": "...",
+    "descriptor_key" : "..."
+  }
+
+header_name
+  *(required, string)* The header name to be queried from the request headers and used to
+  populate the descriptor value for the *descriptor_key*.
+
+descriptor_key
+  *(required, string)* The key to use in the descriptor.
+
+The following descriptor is sent when a header contains a key that matches the *header_name*:
+
+  * ("<descriptor_key>", "<header_value_queried_from_header>")
+
+If *route_key* is set in the :ref:`route <config_http_conn_man_route_table_route_rate_limit>`, the following
+descriptor is sent as well:
+
+  * ("route_key", "<route_key>"), ("<descriptor_key>", "<header_value_queried_from_header>")
+
 Statistics
 ----------
 
diff --git a/include/envoy/router/router.h b/include/envoy/router/router.h
index 7bda1768e22d7..cd4358088b735 100644
--- a/include/envoy/router/router.h
+++ b/include/envoy/router/router.h
@@ -90,6 +90,11 @@ class RateLimitPolicy {
    * @return whether the global rate limiting service should be called for the owning route.
    */
   virtual bool doGlobalLimiting() const PURE;
+
+  /**
+   * @return the route key, if it exists.
+   */
+  virtual const std::string& routeKey() const PURE;
 };
 
 /**
diff --git a/source/common/http/async_client_impl.h b/source/common/http/async_client_impl.h
index 93edfddc25e17..bb9d5ff247c79 100644
--- a/source/common/http/async_client_impl.h
+++ b/source/common/http/async_client_impl.h
@@ -61,6 +61,7 @@ class AsyncRequestImpl final : public AsyncClient::Request,
   struct NullRateLimitPolicy : public Router::RateLimitPolicy {
     // Router::RateLimitPolicy
     bool doGlobalLimiting() const override { return false; }
+    const std::string& routeKey() const override { return EMPTY_STRING; }
   };
 
   struct NullRetryPolicy : public Router::RetryPolicy {
diff --git a/source/common/http/filter/ratelimit.cc b/source/common/http/filter/ratelimit.cc
index 70fb1de03a223..0204413edebcf 100644
--- a/source/common/http/filter/ratelimit.cc
+++ b/source/common/http/filter/ratelimit.cc
@@ -16,7 +16,7 @@ const Http::HeaderMapImpl Filter::TOO_MANY_REQUESTS_HEADER{
 
 void ServiceToServiceAction::populateDescriptors(const Router::RouteEntry& route,
                                                  std::vector<::RateLimit::Descriptor>& descriptors,
-                                                 FilterConfig& config) {
+                                                 FilterConfig& config, const HeaderMap&) {
   // We limit on 2 dimensions.
   // 1) All calls to the given cluster.
   // 2) Calls to the given cluster and from this cluster.
@@ -26,6 +26,24 @@ void ServiceToServiceAction::populateDescriptors(const Router::RouteEntry& route
       {{{"to_cluster", route.clusterName()}, {"from_cluster", config.localServiceCluster()}}});
 }
 
+void RequestHeadersAction::populateDescriptors(const Router::RouteEntry& route,
+                                               std::vector<::RateLimit::Descriptor>& descriptors,
+                                               FilterConfig&, const HeaderMap& headers) {
+  std::string header_value = headers.get(header_name_);
+  if (header_value.empty()) {
+    return;
+  }
+
+  descriptors.push_back({{{descriptor_key_, header_value}}});
+
+  const std::string& route_key = route.rateLimitPolicy().routeKey();
+  if (route_key.empty()) {
+    return;
+  }
+
+  descriptors.push_back({{{"route_key", route_key}, {descriptor_key_, header_value}}});
+}
+
 FilterConfig::FilterConfig(const Json::Object& config, const std::string& local_service_cluster,
                            Stats::Store& stats_store, Runtime::Loader& runtime)
     : domain_(config.getString("domain")), local_service_cluster_(local_service_cluster),
@@ -34,6 +52,8 @@ FilterConfig::FilterConfig(const Json::Object& config, const std::string& local_
     std::string type = action.getString("type");
     if (type == "service_to_service") {
       actions_.emplace_back(new ServiceToServiceAction());
+    } else if (type == "request_headers") {
+      actions_.emplace_back(new RequestHeadersAction(action));
     } else {
       throw EnvoyException(fmt::format("unknown http rate limit filter action '{}'", type));
     }
@@ -49,7 +69,7 @@ FilterHeadersStatus Filter::decodeHeaders(HeaderMap& headers, bool) {
   if (route && route->rateLimitPolicy().doGlobalLimiting()) {
     std::vector<::RateLimit::Descriptor> descriptors;
     for (const ActionPtr& action : config_->actions()) {
-      action->populateDescriptors(*route, descriptors, *config_);
+      action->populateDescriptors(*route, descriptors, *config_, headers);
     }
 
     if (!descriptors.empty()) {
diff --git a/source/common/http/filter/ratelimit.h b/source/common/http/filter/ratelimit.h
index 5ae78cea4e6eb..ba0297a0b7973 100644
--- a/source/common/http/filter/ratelimit.h
+++ b/source/common/http/filter/ratelimit.h
@@ -27,7 +27,7 @@ class Action {
    */
   virtual void populateDescriptors(const Router::RouteEntry& route,
                                    std::vector<::RateLimit::Descriptor>& descriptors,
-                                   FilterConfig& config) PURE;
+                                   FilterConfig& config, const HeaderMap& headers) PURE;
 };
 
 typedef std::unique_ptr<Action> ActionPtr;
@@ -39,10 +39,27 @@ class ServiceToServiceAction : public Action {
 public:
   // Action
   void populateDescriptors(const Router::RouteEntry& route,
-                           std::vector<::RateLimit::Descriptor>& descriptors,
-                           FilterConfig& config) override;
+                           std::vector<::RateLimit::Descriptor>& descriptors, FilterConfig& config,
+                           const HeaderMap&) override;
 };
 
+/**
+ * Action for request headers rate limiting.
+ */
+class RequestHeadersAction : public Action {
+public:
+  RequestHeadersAction(const Json::Object& action)
+      : header_name_(action.getString("header_name")),
+        descriptor_key_(action.getString("descriptor_key")) {}
+  // Action
+  void populateDescriptors(const Router::RouteEntry& route,
+                           std::vector<::RateLimit::Descriptor>& descriptors, FilterConfig& config,
+                           const HeaderMap& headers) override;
+
+private:
+  const LowerCaseString header_name_;
+  const std::string descriptor_key_;
+};
 /**
  * Global configuration for the HTTP rate limit filter.
  */
diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc
index a3fd0b5eedfeb..57d50d28f7adc 100644
--- a/source/common/router/config_impl.cc
+++ b/source/common/router/config_impl.cc
@@ -28,14 +28,6 @@ RetryPolicyImpl::RetryPolicyImpl(const Json::Object& config) {
   retry_on_ = RetryStateImpl::parseRetryOn(config.getObject("retry_policy").getString("retry_on"));
 }
 
-RateLimitPolicyImpl::RateLimitPolicyImpl(const Json::Object& config) {
-  if (!config.hasObject("rate_limit")) {
-    return;
-  }
-
-  do_global_limiting_ = config.getObject("rate_limit").getBoolean("global", false);
-}
-
 ShadowPolicyImpl::ShadowPolicyImpl(const Json::Object& config) {
   if (!config.hasObject("shadow")) {
     return;
diff --git a/source/common/router/config_impl.h b/source/common/router/config_impl.h
index dc02bb309b03d..5957468adeb06 100644
--- a/source/common/router/config_impl.h
+++ b/source/common/router/config_impl.h
@@ -122,13 +122,19 @@ class RetryPolicyImpl : public RetryPolicy {
  */
 class RateLimitPolicyImpl : public RateLimitPolicy {
 public:
-  RateLimitPolicyImpl(const Json::Object& config);
+  RateLimitPolicyImpl(const Json::Object& config)
+      : do_global_limiting_(config.getObject("rate_limit", true).getBoolean("global", false)),
+        route_key_(config.getObject("rate_limit", true).getString("route_key", "")) {}
 
   // Router::RateLimitPolicy
   bool doGlobalLimiting() const override { return do_global_limiting_; }
 
+  // Router::RateLimitPolicy
+  const std::string& routeKey() const override { return route_key_; }
+
 private:
-  bool do_global_limiting_{};
+  const bool do_global_limiting_;
+  const std::string route_key_;
 };
 
 /**
diff --git a/test/common/http/filter/ratelimit_test.cc b/test/common/http/filter/ratelimit_test.cc
index 41f16803e1667..501394b4cc0e5 100644
--- a/test/common/http/filter/ratelimit_test.cc
+++ b/test/common/http/filter/ratelimit_test.cc
@@ -16,7 +16,7 @@ using testing::WithArgs;
 namespace Http {
 namespace RateLimit {
 
-TEST(HttpRateLimitFilterBadConfigTest, All) {
+TEST(HttpRateLimitFilterBadConfigTest, BadType) {
   std::string json = R"EOF(
   {
     "domain": "foo",
@@ -32,23 +32,35 @@ TEST(HttpRateLimitFilterBadConfigTest, All) {
   EXPECT_THROW(FilterConfig(config, "service_cluster", stats_store, runtime), EnvoyException);
 }
 
+TEST(HttpRateLimitFilterBadConfigTest, NoDescriptorKey) {
+  std::string json = R"EOF(
+  {
+    "domain": "foo",
+    "actions": [
+      {
+        "type": "request_headers",
+        "header_name" : "test"
+      }
+    ]
+  }
+  )EOF";
+
+  Json::StringLoader config(json);
+  Stats::IsolatedStoreImpl stats_store;
+  NiceMock<Runtime::MockLoader> runtime;
+  EXPECT_THROW(FilterConfig(config, "service_cluster", stats_store, runtime), EnvoyException);
+}
+
 class HttpRateLimitFilterTest : public testing::Test {
 public:
   HttpRateLimitFilterTest() {
-    std::string json = R"EOF(
-    {
-      "domain": "foo",
-      "actions": [
-        {"type": "service_to_service"}
-      ]
-    }
-    )EOF";
-
     ON_CALL(runtime_.snapshot_, featureEnabled("ratelimit.http_filter_enabled", 100))
         .WillByDefault(Return(true));
     ON_CALL(runtime_.snapshot_, featureEnabled("ratelimit.http_filter_enforcing", 100))
         .WillByDefault(Return(true));
+  }
 
+  void SetUpTest(const std::string json) {
     Json::StringLoader config(json);
     config_.reset(new FilterConfig(config, "service_cluster", stats_store_, runtime_));
 
@@ -57,6 +69,28 @@ class HttpRateLimitFilterTest : public testing::Test {
     filter_->setDecoderFilterCallbacks(filter_callbacks_);
   }
 
+  const std::string service_to_service_json = R"EOF(
+    {
+      "domain": "foo",
+      "actions": [
+        {"type": "service_to_service"}
+      ]
+    }
+    )EOF";
+
+  const std::string request_headers_json = R"EOF(
+    {
+      "domain": "foobar",
+      "actions": [
+        {
+          "type": "request_headers",
+          "header_name": "x-header-name",
+          "descriptor_key" : "my_header_name"
+        }
+      ]
+    }
+    )EOF";
+
   FilterConfigPtr config_;
   ::RateLimit::MockClient* client_;
   std::unique_ptr<Filter> filter_;
@@ -69,6 +103,8 @@ class HttpRateLimitFilterTest : public testing::Test {
 };
 
 TEST_F(HttpRateLimitFilterTest, NoRoute) {
+  SetUpTest(service_to_service_json);
+
   EXPECT_CALL(filter_callbacks_.route_table_, routeForRequest(_)).WillOnce(Return(nullptr));
 
   EXPECT_EQ(FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false));
@@ -77,12 +113,16 @@ TEST_F(HttpRateLimitFilterTest, NoRoute) {
 }
 
 TEST_F(HttpRateLimitFilterTest, NoLimiting) {
+  SetUpTest(service_to_service_json);
+
   EXPECT_EQ(FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false));
   EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, false));
   EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
 }
 
 TEST_F(HttpRateLimitFilterTest, RuntimeDisabled) {
+  SetUpTest(service_to_service_json);
+
   EXPECT_CALL(runtime_.snapshot_, featureEnabled("ratelimit.http_filter_enabled", 100))
       .WillOnce(Return(false));
   EXPECT_EQ(FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false));
@@ -91,6 +131,7 @@ TEST_F(HttpRateLimitFilterTest, RuntimeDisabled) {
 }
 
 TEST_F(HttpRateLimitFilterTest, OkResponse) {
+  SetUpTest(service_to_service_json);
   InSequence s;
 
   filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
@@ -114,6 +155,7 @@ TEST_F(HttpRateLimitFilterTest, OkResponse) {
 }
 
 TEST_F(HttpRateLimitFilterTest, ImmediateOkResponse) {
+  SetUpTest(service_to_service_json);
   InSequence s;
 
   filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
@@ -136,6 +178,7 @@ TEST_F(HttpRateLimitFilterTest, ImmediateOkResponse) {
 }
 
 TEST_F(HttpRateLimitFilterTest, ErrorResponse) {
+  SetUpTest(service_to_service_json);
   InSequence s;
 
   filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
@@ -156,6 +199,7 @@ TEST_F(HttpRateLimitFilterTest, ErrorResponse) {
 }
 
 TEST_F(HttpRateLimitFilterTest, LimitResponse) {
+  SetUpTest(service_to_service_json);
   InSequence s;
 
   filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
@@ -177,6 +221,7 @@ TEST_F(HttpRateLimitFilterTest, LimitResponse) {
 }
 
 TEST_F(HttpRateLimitFilterTest, LimitResponseRuntimeDisabled) {
+  SetUpTest(service_to_service_json);
   InSequence s;
 
   filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
@@ -201,6 +246,7 @@ TEST_F(HttpRateLimitFilterTest, LimitResponseRuntimeDisabled) {
 }
 
 TEST_F(HttpRateLimitFilterTest, ResetDuringCall) {
+  SetUpTest(service_to_service_json);
   InSequence s;
 
   filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
@@ -215,5 +261,63 @@ TEST_F(HttpRateLimitFilterTest, ResetDuringCall) {
   filter_callbacks_.reset_callback_();
 }
 
+TEST_F(HttpRateLimitFilterTest, RequestHeaderOkResponse) {
+  SetUpTest(request_headers_json);
+
+  filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
+
+  EXPECT_CALL(*client_,
+              limit(_, "foobar", testing::ContainerEq(std::vector<::RateLimit::Descriptor>{
+                                     {{{"my_header_name", "test_value"}}}})))
+      .WillOnce(WithArgs<0>(Invoke([&](::RateLimit::RequestCallbacks& callbacks)
+                                       -> void { request_callbacks_ = &callbacks; })));
+
+  HeaderMapImpl request_header{{"x-header-name", "test_value"}};
+  EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_header, false));
+  EXPECT_EQ(FilterDataStatus::StopIterationAndBuffer, filter_->decodeData(data_, false));
+  EXPECT_EQ(FilterTrailersStatus::StopIteration, filter_->decodeTrailers(request_header));
+
+  EXPECT_CALL(filter_callbacks_, continueDecoding());
+  request_callbacks_->complete(::RateLimit::LimitStatus::OK);
+
+  EXPECT_EQ(1U, stats_store_.counter("cluster.fake_cluster.ratelimit.ok").value());
+}
+
+TEST_F(HttpRateLimitFilterTest, RateLimitKeyOkResponse) {
+  SetUpTest(request_headers_json);
+
+  filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
+  filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.route_key_ = "test_key";
+
+  EXPECT_CALL(
+      *client_,
+      limit(_, "foobar", testing::ContainerEq(std::vector<::RateLimit::Descriptor>{
+                             {{{"my_header_name", "test_value"}}},
+                             {{{"route_key", "test_key"}, {"my_header_name", "test_value"}}}})))
+      .WillOnce(WithArgs<0>(Invoke([&](::RateLimit::RequestCallbacks& callbacks)
+                                       -> void { request_callbacks_ = &callbacks; })));
+
+  HeaderMapImpl request_header{{"x-header-name", "test_value"}};
+  EXPECT_EQ(FilterHeadersStatus::StopIteration, filter_->decodeHeaders(request_header, false));
+  EXPECT_EQ(FilterDataStatus::StopIterationAndBuffer, filter_->decodeData(data_, false));
+  EXPECT_EQ(FilterTrailersStatus::StopIteration, filter_->decodeTrailers(request_header));
+
+  EXPECT_CALL(filter_callbacks_, continueDecoding());
+  request_callbacks_->complete(::RateLimit::LimitStatus::OK);
+
+  EXPECT_EQ(1U, stats_store_.counter("cluster.fake_cluster.ratelimit.ok").value());
+}
+
+TEST_F(HttpRateLimitFilterTest, NoRateLimitHeaderMatch) {
+  SetUpTest(request_headers_json);
+  filter_callbacks_.route_table_.route_entry_.rate_limit_policy_.do_global_limiting_ = true;
+
+  EXPECT_CALL(*client_, limit(_, _, _)).Times(0);
+
+  EXPECT_EQ(FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false));
+  EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, false));
+  EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
+}
+
 } // RateLimit
 } // Http
diff --git a/test/mocks/router/mocks.h b/test/mocks/router/mocks.h
index 84e233ce35d8a..57060a0d539da 100644
--- a/test/mocks/router/mocks.h
+++ b/test/mocks/router/mocks.h
@@ -44,7 +44,11 @@ class TestRateLimitPolicy : public RateLimitPolicy {
   // Router::RateLimitPolicy
   bool doGlobalLimiting() const override { return do_global_limiting_; }
 
+  // Router::RateLimitPolicy
+  const std::string& routeKey() const override { return route_key_; }
+
   bool do_global_limiting_{};
+  std::string route_key_;
 };
 
 class TestShadowPolicy : public ShadowPolicy {
