diff --git a/NEWS b/NEWS
index 6a5d2bf98..b97e7bff7 100644
--- a/NEWS
+++ b/NEWS
@@ -59,6 +59,7 @@ v2.8.0 - xx xxx xxxx
        gateway.
      * Add support for ACL logging.
      * ovn-northd now has native support for active-standby high availability.
+     * Add support for QoS bandwidth limt with DPDK.
    - Tracing with ofproto/trace now traces through recirculation.
    - OVSDB:
      * New support for role-based access control (see ovsdb-server(1)).
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 0a04af7aa..8dbb895f3 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -72,7 +72,8 @@ struct simap;
     OVNACT(PUT_DHCPV6_OPTS,   ovnact_put_dhcp_opts)   \
     OVNACT(SET_QUEUE,         ovnact_set_queue)       \
     OVNACT(DNS_LOOKUP,        ovnact_dns_lookup)      \
-    OVNACT(LOG,               ovnact_log)
+    OVNACT(LOG,               ovnact_log)             \
+    OVNACT(SET_METER,         ovnact_set_meter)
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -274,6 +275,13 @@ struct ovnact_log {
     char *name;
 };
 
+/* OVNACT_SET_METER. */
+struct ovnact_set_meter {
+    struct ovnact ovnact;
+    uint32_t rate;              /* 32-bit rate field. */
+    uint32_t burst;             /* 32-bit burst rate field. */
+};
+
 /* Internal use by the helpers below. */
 void ovnact_init(struct ovnact *, enum ovnact_type, size_t len);
 void *ovnact_put(struct ofpbuf *, enum ovnact_type, size_t len);
@@ -350,6 +358,24 @@ struct group_info {
                          * group_table's 'group_ids' bitmap. */
 };
 
+#define MAX_OVN_METERS 65535
+
+struct meter_table {
+    unsigned long *meter_ids;  /* Used as a bitmap with value set
+                                * for allocated meter ids in either
+                                * desired_meters or existing_meters. */
+    struct hmap desired_meters;
+    struct hmap existing_meters;
+};
+
+struct meter_info {
+    struct hmap_node hmap_node;
+    struct ds meter;
+    uint32_t meter_id;
+    bool new_meter_id;  /* 'True' if 'meter_id' was reserved from
+                         * meter_table's 'meter_ids' bitmap. */
+};
+
 enum action_opcode {
     /* "arp { ...actions... }".
      *
@@ -484,6 +510,9 @@ struct ovnact_encode_params {
     /* A struct to figure out the group_id for group actions. */
     struct group_table *group_table;
 
+    /* A struct to figure out the meter_id for meter actions. */
+    struct meter_table *meter_table;
+
     /* OVN maps each logical flow table (ltable), one-to-one, onto a physical
      * OpenFlow flow table (ptable).  A number of parameters describe this
      * mapping and data related to flow tables:
diff --git a/ovn/controller/lflow.c b/ovn/controller/lflow.c
index 6d9f02cb2..bca92c9d9 100644
--- a/ovn/controller/lflow.c
+++ b/ovn/controller/lflow.c
@@ -62,6 +62,7 @@ static void consider_logical_flow(struct controller_ctx *ctx,
                                   const struct sbrec_logical_flow *lflow,
                                   const struct hmap *local_datapaths,
                                   struct group_table *group_table,
+                                  struct meter_table *meter_table,
                                   const struct sbrec_chassis *chassis,
                                   struct hmap *dhcp_opts,
                                   struct hmap *dhcpv6_opts,
@@ -142,6 +143,7 @@ add_logical_flows(struct controller_ctx *ctx,
                   const struct chassis_index *chassis_index,
                   const struct hmap *local_datapaths,
                   struct group_table *group_table,
+                  struct meter_table *meter_table,
                   const struct sbrec_chassis *chassis,
                   const struct shash *addr_sets,
                   struct hmap *flow_table,
@@ -168,7 +170,7 @@ add_logical_flows(struct controller_ctx *ctx,
     SBREC_LOGICAL_FLOW_FOR_EACH (lflow, ctx->ovnsb_idl) {
         consider_logical_flow(ctx, chassis_index,
                               lflow, local_datapaths,
-                              group_table, chassis,
+                              group_table, meter_table, chassis,
                               &dhcp_opts, &dhcpv6_opts, &conj_id_ofs,
                               addr_sets, flow_table, active_tunnels);
     }
@@ -183,6 +185,7 @@ consider_logical_flow(struct controller_ctx *ctx,
                       const struct sbrec_logical_flow *lflow,
                       const struct hmap *local_datapaths,
                       struct group_table *group_table,
+                      struct meter_table *meter_table,
                       const struct sbrec_chassis *chassis,
                       struct hmap *dhcp_opts,
                       struct hmap *dhcpv6_opts,
@@ -252,6 +255,7 @@ consider_logical_flow(struct controller_ctx *ctx,
         .is_switch = is_switch(ldp),
         .is_gateway_router = is_gateway_router(ldp, local_datapaths),
         .group_table = group_table,
+        .meter_table = meter_table,
 
         .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
         .ingress_ptable = OFTABLE_LOG_INGRESS_PIPELINE,
@@ -411,12 +415,13 @@ lflow_run(struct controller_ctx *ctx,
           const struct chassis_index *chassis_index,
           const struct hmap *local_datapaths,
           struct group_table *group_table,
+          struct meter_table *meter_table,
           const struct shash *addr_sets,
           struct hmap *flow_table,
           struct sset *active_tunnels)
 {
     add_logical_flows(ctx, chassis_index, local_datapaths,
-                      group_table, chassis, addr_sets, flow_table,
+                      group_table, meter_table, chassis, addr_sets, flow_table,
                       active_tunnels);
     add_neighbor_flows(ctx, flow_table);
 }
diff --git a/ovn/controller/lflow.h b/ovn/controller/lflow.h
index 07709fe06..6df957200 100644
--- a/ovn/controller/lflow.h
+++ b/ovn/controller/lflow.h
@@ -38,6 +38,7 @@
 struct chassis_index;
 struct controller_ctx;
 struct group_table;
+struct meter_table;
 struct hmap;
 struct sbrec_chassis;
 struct simap;
@@ -67,6 +68,7 @@ void lflow_run(struct controller_ctx *,
                const struct chassis_index *,
                const struct hmap *local_datapaths,
                struct group_table *group_table,
+               struct meter_table *meter_table,
                const struct shash *addr_sets,
                struct hmap *flow_table,
                struct sset *active_tunnels);
diff --git a/ovn/controller/ofctrl.c b/ovn/controller/ofctrl.c
index fc88a410b..d92258558 100644
--- a/ovn/controller/ofctrl.c
+++ b/ovn/controller/ofctrl.c
@@ -133,6 +133,9 @@ static struct hmap installed_flows;
 /* A reference to the group_table. */
 static struct group_table *groups;
 
+/* A reference to the meter_table. */
+static struct meter_table *meters;
+
 /* MFF_* field ID for our Geneve option.  In S_TLV_TABLE_MOD_SENT, this is
  * the option we requested (we don't know whether we obtained it yet).  In
  * S_CLEAR_FLOWS or S_UPDATE_FLOWS, this is really the option we have. */
@@ -144,13 +147,15 @@ static struct ofpbuf *encode_flow_mod(struct ofputil_flow_mod *);
 
 static struct ofpbuf *encode_group_mod(const struct ofputil_group_mod *);
 
+static struct ofpbuf *encode_meter_mod(const struct ofputil_meter_mod *);
+
 static void ovn_flow_table_clear(struct hmap *flow_table);
 static void ovn_flow_table_destroy(struct hmap *flow_table);
 
 static void ofctrl_recv(const struct ofp_header *, enum ofptype);
 
 void
-ofctrl_init(struct group_table *group_table)
+ofctrl_init(struct group_table *group_table, struct meter_table *meter_table)
 {
     swconn = rconn_create(5, 0, DSCP_DEFAULT, 1 << OFP13_VERSION);
     tx_counter = rconn_packet_counter_create();
@@ -158,6 +163,7 @@ ofctrl_init(struct group_table *group_table)
     ovs_list_init(&flow_updates);
     ovn_init_symtab(&symtab);
     groups = group_table;
+    meters = meter_table;
 }
 
 /* S_NEW, for a new connection.
@@ -388,6 +394,18 @@ run_S_CLEAR_FLOWS(void)
         ovn_group_table_clear(groups, true);
     }
 
+    /* Send a meter_mod to delete all meters. */
+    struct ofputil_meter_mod mm;
+    memset(&mm, 0, sizeof mm);
+    mm.command = OFPMC13_DELETE;
+    mm.meter.meter_id = OFPM13_ALL;
+    queue_msg(encode_meter_mod(&mm));
+
+    /* Clear existing meters, to match the state of the switch. */
+    if (meters) {
+        ovn_meter_table_clear(meters, true);
+    }
+
     /* All flow updates are irrelevant now. */
     struct ofctrl_flow_update *fup, *next;
     LIST_FOR_EACH_SAFE (fup, next, list_node, &flow_updates) {
@@ -797,7 +815,60 @@ add_group_mod(const struct ofputil_group_mod *gm, struct ovs_list *msgs)
     struct ofpbuf *msg = encode_group_mod(gm);
     ovs_list_push_back(msgs, &msg->list_node);
 }
-
+
+/* meter_table. */
+
+/* Finds and returns a meter_info in 'existing_meters' whose key is identical
+ * to 'target''s key, or NULL if there is none. */
+static struct meter_info *
+ovn_meter_lookup(struct hmap *exisiting_meters,
+                 const struct meter_info *target)
+{
+    struct meter_info *e;
+
+    HMAP_FOR_EACH_WITH_HASH(e, hmap_node, target->hmap_node.hash,
+                            exisiting_meters) {
+        if (e->meter_id == target->meter_id) {
+            return e;
+        }
+   }
+    return NULL;
+}
+
+/* Clear either desired_meters or existing_meters in meter_table. */
+void
+ovn_meter_table_clear(struct meter_table *meter_table, bool existing)
+{
+    struct meter_info *m, *next;
+    struct hmap *target_meter = existing
+                                ? &meter_table->existing_meters
+                                : &meter_table->desired_meters;
+
+    HMAP_FOR_EACH_SAFE (m, next, hmap_node, target_meter) {
+        hmap_remove(target_meter, &m->hmap_node);
+        /* Don't unset bitmap for desired meter_info if the meter_id
+         * was not freshly reserved. */
+        if (existing || m->new_meter_id) {
+            bitmap_set0(meter_table->meter_ids, m->meter_id);
+        }
+        ds_destroy(&m->meter);
+        free(m);
+    }
+}
+
+static struct ofpbuf *
+encode_meter_mod(const struct ofputil_meter_mod *mm)
+{
+    return ofputil_encode_meter_mod(OFP13_VERSION, mm);
+}
+
+static void
+add_meter_mod(const struct ofputil_meter_mod *mm, struct ovs_list *msgs)
+{
+    struct ofpbuf *msg = encode_meter_mod(mm);
+    ovs_list_push_back(msgs, &msg->list_node);
+}
+
 static void
 add_ct_flush_zone(uint16_t zone_id, struct ovs_list *msgs)
 {
@@ -833,6 +904,12 @@ ofctrl_can_put(void)
  * 'groups->desired_groups' and frees them. (The hmap itself isn't
  * destroyed.)
  *
+ * Replaces the meter table on the switch, if possible, by the contents of
+ * 'meters->desired_meters'.  Regardless of whether the meter table
+ * is updated, this deletes all the meters from the
+ * 'meters->desired_meters' and frees them. (The hmap itself isn't
+ * destroyed.)
+ *
  * Sends conntrack flush messages to each zone in 'pending_ct_zones' that
  * is in the CT_ZONE_OF_QUEUED state and then moves the zone into the
  * CT_ZONE_OF_SENT state.
@@ -891,6 +968,35 @@ ofctrl_put(struct hmap *flow_table, struct shash *pending_ct_zones,
         }
     }
 
+    /* Iterate through all the desired meters. If there are new ones,
+     * add them to the switch. */
+    struct meter_info *desired_meter;
+    HMAP_FOR_EACH(desired_meter, hmap_node, &meters->desired_meters) {
+        if (!ovn_meter_lookup(&meters->existing_meters, desired_meter)
+            && desired_meter->meter_id) {
+            /* Create and install new meter. */
+            struct ofputil_meter_mod mm;
+            enum ofputil_protocol usable_protocols;
+            char *error;
+            struct ds meter_string = DS_EMPTY_INITIALIZER;
+            ds_put_format(&meter_string, "meter=%u,%s",
+                          desired_meter->meter_id,
+                          ds_cstr(&desired_meter->meter));
+
+            error = parse_ofp_meter_mod_str(&mm, ds_cstr(&meter_string),
+                                            OFPMC13_ADD, &usable_protocols);
+            if (!error) {
+                add_meter_mod(&mm, &msgs);
+            } else {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_ERR_RL(&rl, "new meter %s %s", error,
+                         ds_cstr(&meter_string));
+                free(error);
+            }
+            ds_destroy(&meter_string);
+        }
+    }
+
     /* Iterate through all of the installed flows.  If any of them are no
      * longer desired, delete them; if any of them should have different
      * actions, update them. */
@@ -1012,6 +1118,54 @@ ofctrl_put(struct hmap *flow_table, struct shash *pending_ct_zones,
         }
     }
 
+    /* Iterate through the installed meters from previous runs. If they
+     * are not needed delete them. */
+    struct meter_info *installed_meter, *next_meter;
+    HMAP_FOR_EACH_SAFE(installed_meter, next_meter, hmap_node,
+                       &meters->existing_meters) {
+        if (!ovn_meter_lookup(&meters->desired_meters, installed_meter)) {
+            /* Delete the meter. */
+            struct ofputil_meter_mod mm;
+            enum ofputil_protocol usable_protocols;
+            char *error;
+            struct ds meter_string = DS_EMPTY_INITIALIZER;
+            ds_put_format(&meter_string, "meter=%u", installed_meter->meter_id);
+
+            error = parse_ofp_meter_mod_str(&mm, ds_cstr(&meter_string),
+                                            OFPMC13_DELETE, &usable_protocols);
+            if (!error) {
+                add_meter_mod(&mm, &msgs);
+            } else {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_ERR_RL(&rl, "Error deleting meter %d: %s",
+                         installed_meter->meter_id, error);
+                free(error);
+            }
+            ds_destroy(&meter_string);
+
+            /* Remove 'installed_meter' from 'meters->existing_meters' */
+            hmap_remove(&meters->existing_meters, &installed_meter->hmap_node);
+            ds_destroy(&installed_meter->meter);
+
+            /* Dealloc meter_id. */
+            bitmap_set0(meters->meter_ids, installed_meter->meter_id);
+            free(installed_meter);
+        }
+    }
+
+    /* Move the contents of desired_meters to existing_meters. */
+    HMAP_FOR_EACH_SAFE(desired_meter, next_meter, hmap_node,
+                       &meters->desired_meters) {
+        hmap_remove(&meters->desired_meters, &desired_meter->hmap_node);
+        if (!ovn_meter_lookup(&meters->existing_meters, desired_meter)) {
+            hmap_insert(&meters->existing_meters, &desired_meter->hmap_node,
+                        desired_meter->hmap_node.hash);
+        } else {
+           ds_destroy(&desired_meter->meter);
+           free(desired_meter);
+        }
+    }
+
     if (!ovs_list_is_empty(&msgs)) {
         /* Add a barrier to the list of messages. */
         struct ofpbuf *barrier = ofputil_encode_barrier_request(OFP13_VERSION);
diff --git a/ovn/controller/ofctrl.h b/ovn/controller/ofctrl.h
index d83f6aec4..e680e2d61 100644
--- a/ovn/controller/ofctrl.h
+++ b/ovn/controller/ofctrl.h
@@ -24,6 +24,7 @@
 
 struct controller_ctx;
 struct group_table;
+struct meter_table;
 struct hmap;
 struct match;
 struct ofpbuf;
@@ -31,7 +32,7 @@ struct ovsrec_bridge;
 struct shash;
 
 /* Interface for OVN main loop. */
-void ofctrl_init(struct group_table *group_table);
+void ofctrl_init(struct group_table *group_table, struct meter_table *meter_table);
 enum mf_field_id ofctrl_run(const struct ovsrec_bridge *br_int,
                             struct shash *pending_ct_zones);
 bool ofctrl_can_put(void);
@@ -58,4 +59,7 @@ void ofctrl_flow_table_clear(void);
 void ovn_group_table_clear(struct group_table *group_table,
                            bool existing);
 
+void ovn_meter_table_clear(struct meter_table *meter_table,
+                           bool existing);
+
 #endif /* ovn/ofctrl.h */
diff --git a/ovn/controller/ovn-controller.c b/ovn/controller/ovn-controller.c
index e2c965290..729cf5c64 100644
--- a/ovn/controller/ovn-controller.c
+++ b/ovn/controller/ovn-controller.c
@@ -599,9 +599,16 @@ main(int argc, char *argv[])
     hmap_init(&group_table.desired_groups);
     hmap_init(&group_table.existing_groups);
 
+    /* Initialize meter ids for QoS. */
+    struct meter_table meter_table;
+    meter_table.meter_ids = bitmap_allocate(MAX_OVN_METERS);
+    bitmap_set1(meter_table.meter_ids, 0); /* Meter id 0 is invalid. */
+    hmap_init(&meter_table.desired_meters);
+    hmap_init(&meter_table.existing_meters);
+
     daemonize_complete();
 
-    ofctrl_init(&group_table);
+    ofctrl_init(&group_table, &meter_table);
     pinctrl_init();
     lflow_init();
 
@@ -708,7 +715,8 @@ main(int argc, char *argv[])
                     struct hmap flow_table = HMAP_INITIALIZER(&flow_table);
                     lflow_run(&ctx, chassis,
                               &chassis_index, &local_datapaths, &group_table,
-                              &addr_sets, &flow_table, &active_tunnels);
+                              &meter_table, &addr_sets, &flow_table,
+                              &active_tunnels);
 
                     if (chassis_id) {
                         bfd_run(&ctx, br_int, chassis, &local_datapaths,
@@ -851,6 +859,18 @@ main(int argc, char *argv[])
     }
     hmap_destroy(&group_table.existing_groups);
 
+    bitmap_free(meter_table.meter_ids);
+    hmap_destroy(&meter_table.desired_meters);
+
+    struct meter_info *installed_meter, *next_meter;
+    HMAP_FOR_EACH_SAFE(installed_meter, next_meter, hmap_node,
+                       &meter_table.existing_meters) {
+        hmap_remove(&meter_table.existing_meters, &installed_meter->hmap_node);
+        ds_destroy(&installed_meter->meter);
+        free(installed_meter);
+    }
+    hmap_destroy(&meter_table.existing_meters);
+
     ovsdb_idl_loop_destroy(&ovs_idl_loop);
     ovsdb_idl_loop_destroy(&ovnsb_idl_loop);
 
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index d0d73b69c..9c1f5f963 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -1873,6 +1873,118 @@ ovnact_log_free(struct ovnact_log *log)
     free(log->name);
 }
 
+static void
+parse_set_meter_action(struct action_context *ctx)
+{
+    int rate;
+    int burst = 0;
+
+    if (ctx->pp->cur_ltable >= ctx->pp->n_tables) {
+        lexer_error(ctx->lexer,
+                    "\"set_meter\" action not allowed in last table.");
+        return;
+    }
+
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+    lexer_force_int(ctx->lexer, &rate);
+    if (lexer_match(ctx->lexer, LEX_T_COMMA)) {
+        lexer_force_int(ctx->lexer, &burst);
+    }
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+
+    struct ovnact_set_meter *cl = ovnact_put_SET_METER(ctx->ovnacts);
+    cl->rate = (uint32_t)rate;
+    cl->burst = (uint32_t)burst;
+}
+
+static void
+format_SET_METER(const struct ovnact_set_meter *cl, struct ds *s)
+{
+    if (cl->burst) {
+        ds_put_format(s, "set_meter(%d ,%d);", cl->rate, cl->burst);
+    } else {
+        ds_put_format(s, "set_meter(%d);", cl->rate);
+    }
+}
+
+static void
+encode_SET_METER(const struct ovnact_set_meter *cl,
+                 const struct ovnact_encode_params *ep,
+                 struct ofpbuf *ofpacts)
+{
+    uint32_t meter_id = 0, hash;
+    struct meter_info *meter_info;
+    struct ofpact_meter *om;
+
+    struct ds ds = DS_EMPTY_INITIALIZER;
+    if (cl->burst) {
+        ds_put_format(&ds,
+                      "kbps burst stats bands=type=drop rate=%d burst_size=%d",
+                      cl->rate, cl->burst);
+    } else {
+        ds_put_format(&ds, "kbps stats bands=type=drop rate=%d", cl->rate);
+    }
+
+    hash = hash_string(ds_cstr(&ds), 0);
+
+    /* Check whether we have non installed but allocated meter_id. */
+    HMAP_FOR_EACH_WITH_HASH (meter_info, hmap_node, hash,
+                             &ep->meter_table->desired_meters) {
+        if (!strcmp(ds_cstr(&meter_info->meter), ds_cstr(&ds))) {
+            meter_id = meter_info->meter_id;
+            break;
+        }
+    }
+
+    if (!meter_id) {
+        /* Check whether we already have an installed entry for this
+         * combination. */
+        HMAP_FOR_EACH_WITH_HASH (meter_info, hmap_node, hash,
+                                 &ep->meter_table->existing_meters) {
+            if (!strcmp(ds_cstr(&meter_info->meter), ds_cstr(&ds))) {
+                meter_id = meter_info->meter_id;
+            }
+        }
+
+        bool new_meter_id = false;
+        if (!meter_id) {
+            /* Reserve a new meter_id. */
+            meter_id = bitmap_scan(ep->meter_table->meter_ids, 0, 1,
+                                   MAX_OVN_METERS + 1);
+            new_meter_id = true;
+        }
+
+        if (meter_id == MAX_OVN_METERS + 1) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+            VLOG_ERR_RL(&rl, "out of meter ids");
+
+            ds_destroy(&ds);
+            return;
+        }
+        bitmap_set1(ep->meter_table->meter_ids, meter_id);
+
+        meter_info = xmalloc(sizeof *meter_info);
+        meter_info->meter = ds;
+        meter_info->meter_id = meter_id;
+        meter_info->hmap_node.hash = hash;
+        meter_info->new_meter_id = new_meter_id;
+
+        hmap_insert(&ep->meter_table->desired_meters,
+                    &meter_info->hmap_node, meter_info->hmap_node.hash);
+    } else {
+        ds_destroy(&ds);
+    }
+
+    /* Create an action to set the meter. */
+    om = ofpact_put_METER(ofpacts);
+    om->meter_id = meter_id;
+}
+
+static void
+ovnact_set_meter_free(struct ovnact_set_meter *ct OVS_UNUSED)
+{
+}
+
 /* Parses an assignment or exchange or put_dhcp_opts action. */
 static void
 parse_set_action(struct action_context *ctx)
@@ -1954,6 +2066,8 @@ parse_action(struct action_context *ctx)
         parse_SET_QUEUE(ctx);
     } else if (lexer_match_id(ctx->lexer, "log")) {
         parse_LOG(ctx);
+    } else if (lexer_match_id(ctx->lexer, "set_meter")) {
+        parse_set_meter_action(ctx);
     } else {
         lexer_syntax_error(ctx->lexer, "expecting action");
     }
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 49e4ac338..593909d10 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -108,25 +108,27 @@ enum ovn_stage {
     PIPELINE_STAGE(SWITCH, IN,  PRE_STATEFUL,   5, "ls_in_pre_stateful")  \
     PIPELINE_STAGE(SWITCH, IN,  ACL,            6, "ls_in_acl")           \
     PIPELINE_STAGE(SWITCH, IN,  QOS_MARK,       7, "ls_in_qos_mark")      \
-    PIPELINE_STAGE(SWITCH, IN,  LB,             8, "ls_in_lb")            \
-    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,       9, "ls_in_stateful")      \
-    PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,    10, "ls_in_arp_rsp")       \
-    PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,  11, "ls_in_dhcp_options")  \
-    PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE, 12, "ls_in_dhcp_response") \
-    PIPELINE_STAGE(SWITCH, IN,  DNS_LOOKUP,      13, "ls_in_dns_lookup") \
-    PIPELINE_STAGE(SWITCH, IN,  DNS_RESPONSE,  14, "ls_in_dns_response") \
-    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,       15, "ls_in_l2_lkup")       \
-                                                                      \
-    /* Logical switch egress stages. */                               \
-    PIPELINE_STAGE(SWITCH, OUT, PRE_LB,       0, "ls_out_pre_lb")     \
-    PIPELINE_STAGE(SWITCH, OUT, PRE_ACL,      1, "ls_out_pre_acl")     \
-    PIPELINE_STAGE(SWITCH, OUT, PRE_STATEFUL, 2, "ls_out_pre_stateful")  \
-    PIPELINE_STAGE(SWITCH, OUT, LB,           3, "ls_out_lb")            \
+    PIPELINE_STAGE(SWITCH, IN,  QOS_METER,      8, "ls_in_qos_meter")     \
+    PIPELINE_STAGE(SWITCH, IN,  LB,             9, "ls_in_lb")            \
+    PIPELINE_STAGE(SWITCH, IN,  STATEFUL,      10, "ls_in_stateful")      \
+    PIPELINE_STAGE(SWITCH, IN,  ARP_ND_RSP,    11, "ls_in_arp_rsp")       \
+    PIPELINE_STAGE(SWITCH, IN,  DHCP_OPTIONS,  12, "ls_in_dhcp_options")  \
+    PIPELINE_STAGE(SWITCH, IN,  DHCP_RESPONSE, 13, "ls_in_dhcp_response") \
+    PIPELINE_STAGE(SWITCH, IN,  DNS_LOOKUP,    14, "ls_in_dns_lookup")    \
+    PIPELINE_STAGE(SWITCH, IN,  DNS_RESPONSE,  15, "ls_in_dns_response")  \
+    PIPELINE_STAGE(SWITCH, IN,  L2_LKUP,       16, "ls_in_l2_lkup")       \
+                                                                          \
+    /* Logical switch egress stages. */                                   \
+    PIPELINE_STAGE(SWITCH, OUT, PRE_LB,       0, "ls_out_pre_lb")         \
+    PIPELINE_STAGE(SWITCH, OUT, PRE_ACL,      1, "ls_out_pre_acl")        \
+    PIPELINE_STAGE(SWITCH, OUT, PRE_STATEFUL, 2, "ls_out_pre_stateful")   \
+    PIPELINE_STAGE(SWITCH, OUT, LB,           3, "ls_out_lb")             \
     PIPELINE_STAGE(SWITCH, OUT, ACL,          4, "ls_out_acl")            \
     PIPELINE_STAGE(SWITCH, OUT, QOS_MARK,     5, "ls_out_qos_mark")       \
-    PIPELINE_STAGE(SWITCH, OUT, STATEFUL,     6, "ls_out_stateful")       \
-    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_IP,  7, "ls_out_port_sec_ip")    \
-    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_L2,  8, "ls_out_port_sec_l2")    \
+    PIPELINE_STAGE(SWITCH, OUT, QOS_METER,    6, "ls_out_qos_meter")      \
+    PIPELINE_STAGE(SWITCH, OUT, STATEFUL,     7, "ls_out_stateful")       \
+    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_IP,  8, "ls_out_port_sec_ip")    \
+    PIPELINE_STAGE(SWITCH, OUT, PORT_SEC_L2,  9, "ls_out_port_sec_l2")    \
                                                                       \
     /* Logical router ingress stages. */                              \
     PIPELINE_STAGE(ROUTER, IN,  ADMISSION,   0, "lr_in_admission")    \
@@ -3364,21 +3366,49 @@ static void
 build_qos(struct ovn_datapath *od, struct hmap *lflows) {
     ovn_lflow_add(lflows, od, S_SWITCH_IN_QOS_MARK, 0, "1", "next;");
     ovn_lflow_add(lflows, od, S_SWITCH_OUT_QOS_MARK, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_QOS_METER, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_SWITCH_OUT_QOS_METER, 0, "1", "next;");
 
     for (size_t i = 0; i < od->nbs->n_qos_rules; i++) {
         struct nbrec_qos *qos = od->nbs->qos_rules[i];
         bool ingress = !strcmp(qos->direction, "from-lport") ? true :false;
         enum ovn_stage stage = ingress ? S_SWITCH_IN_QOS_MARK : S_SWITCH_OUT_QOS_MARK;
+        uint32_t rate = 0;
+        uint32_t burst = 0;
+
+        for (size_t j = 0; j < qos->n_action; j++) {
+            if (!strcmp(qos->key_action[j], "dscp")) {
+                struct ds dscp_action = DS_EMPTY_INITIALIZER;
+
+                ds_put_format(&dscp_action, "ip.dscp = %d; next;",
+                              (uint8_t)qos->value_action[j]);
+                ovn_lflow_add(lflows, od, stage,
+                              qos->priority,
+                              qos->match, ds_cstr(&dscp_action));
+                ds_destroy(&dscp_action);
+            }
+        }
 
-        if (!strcmp(qos->key_action, "dscp")) {
-            struct ds dscp_action = DS_EMPTY_INITIALIZER;
-
-            ds_put_format(&dscp_action, "ip.dscp = %d; next;",
-                          (uint8_t)qos->value_action);
+        for (size_t n = 0; n < qos->n_bandwidth; n++) {
+            if (!strcmp(qos->key_bandwidth[n], "rate")) {
+                rate = (uint32_t)qos->value_bandwidth[n];
+            } else if (!strcmp(qos->key_bandwidth[n], "burst")) {
+                burst = (uint32_t)qos->value_bandwidth[n];
+            }
+        }
+        if (rate) {
+            struct ds meter_action = DS_EMPTY_INITIALIZER;
+            stage = ingress ? S_SWITCH_IN_QOS_METER : S_SWITCH_OUT_QOS_METER;
+            if (burst) {
+                ds_put_format(&meter_action, "set_meter(%d, %d); next;",
+                              rate, burst);
+            } else {
+                ds_put_format(&meter_action, "set_meter(%d); next;", rate);
+            }
             ovn_lflow_add(lflows, od, stage,
                           qos->priority,
-                          qos->match, ds_cstr(&dscp_action));
-            ds_destroy(&dscp_action);
+                          qos->match, ds_cstr(&meter_action));
+            ds_destroy(&meter_action);
         }
     }
 }
diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema
index a077bfb81..23ab008cd 100644
--- a/ovn/ovn-nb.ovsschema
+++ b/ovn/ovn-nb.ovsschema
@@ -1,7 +1,7 @@
 {
     "name": "OVN_Northbound",
     "version": "5.8.0",
-    "cksum": "2812300190 16766",
+    "cksum": "3886656843 17257",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -164,7 +164,14 @@
                                             "enum": ["set", ["dscp"]]},
                                     "value": {"type": "integer",
                                               "minInteger": 0,
-                                              "maxInteger": 63}}},
+                                              "maxInteger": 63},
+                                    "min": 0, "max": "unlimited"}},
+                "bandwidth": {"type": {"key": {"type": "string",
+                                               "enum": ["set", ["rate", "burst"]]},
+                                       "value": {"type": "integer",
+                                                 "minInteger": 1,
+                                                 "maxInteger": 65535},
+                                       "min": 0, "max": "unlimited"}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index 9869d7ed7..c666ec2c4 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -1250,6 +1250,22 @@
       </ul>
     </column>
 
+    <column name="bandwidth">
+      <p>
+         The bandwidth limit to be performed on the matched packet. 
+         Currently only supported in the userspace by dpdk.
+      </p>
+      <ul>
+        <li>
+          <code>rate</code>: The value of rate limit.
+        </li>
+        <li>
+          <code>burst</code>: The value of burst rate limit. This is optional
+          and needs to specify the <code>rate</code> first.
+        </li>
+      </ul>
+    </column>
+
     <column name="external_ids">
       See <em>External IDs</em> at the beginning of this document.
     </column>
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 0a894f8cb..ee1db3f6f 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1516,6 +1516,21 @@
             <b>Prerequisite:</b> <code>udp</code>
           </p>
         </dd>
+
+        <dt><code>set_meter(<var>rate</var>);</code></dt>
+        <dt><code>set_meter(<var>rate</var>, <var>burst</var>);</code></dt>
+        <dd>
+          <p>
+            <b>Parameters</b>: rate limit int field <var>rate</var>, burst rate limits
+            int field <var>burst</var>.
+          </p>
+
+          <p>
+            This action sets the rate limit for a flow.
+          </p>
+
+          <p><b>Example:</b> <code>set_meter(100, 1000);</code></p>
+        </dd>
       </dl>
 
       <dl>
diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
index 59083eebe..bb5593876 100644
--- a/ovn/utilities/ovn-trace.c
+++ b/ovn/utilities/ovn-trace.c
@@ -1833,6 +1833,10 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
         case OVNACT_LOG:
             execute_log(ovnact_get_LOG(a), uflow, super);
             break;
+
+        case OVNACT_SET_METER:
+            /* Nothing to do. */
+            break;
         }
 
     }
diff --git a/tests/ovn.at b/tests/ovn.at
index 6c38b973f..0e34f7df4 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -5885,7 +5885,7 @@ OVN_CLEANUP([hv])
 AT_CLEANUP
 
 
-AT_SETUP([ovn -- DSCP marking check])
+AT_SETUP([ovn -- DSCP marking and meter check])
 AT_KEYWORDS([ovn])
 ovn_start
 
@@ -5952,13 +5952,16 @@ AT_CHECK([get_final_nw_tos], [0], [none
 check_tos 0
 
 # Mark DSCP with a valid value
-qos_id=$(ovn-nbctl --wait=hv -- --id=@lp1-qos create QoS priority=100 action=dscp=48 match="inport\=\=\"lp1\"" direction="from-lport" -- set Logical_Switch lsw0 qos_rules=@lp1-qos)
+qos_id=$(ovn-nbctl --wait=hv -- --id=@lp1-qos create QoS priority=100 action=dscp=48 bandwidth=rate=100,burst=1000 match="inport\=\=\"lp1\"" direction="from-lport" -- set Logical_Switch lsw0 qos_rules=@lp1-qos)
 check_tos 48
 
 # Update the DSCP marking
 ovn-nbctl --wait=hv set QoS $qos_id action=dscp=63
 check_tos 63
 
+# Update the meter rate
+ovn-nbctl --wait=hv set QoS $qos_id bandwidth=rate=65535,burst=65535
+
 ovn-nbctl --wait=hv set QoS $qos_id match="outport\=\=\"lp2\"" direction="to-lport"
 check_tos 63
 
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index 4beb2b8d6..e9dcba231 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -1206,6 +1206,13 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
     hmap_init(&group_table.desired_groups);
     hmap_init(&group_table.existing_groups);
 
+    /* Initialize meter ids for QoS. */
+    struct meter_table meter_table;
+    meter_table.meter_ids = bitmap_allocate(MAX_OVN_METERS);
+    bitmap_set1(meter_table.meter_ids, 0); /* Meter id 0 is invalid. */
+    hmap_init(&meter_table.desired_meters);
+    hmap_init(&meter_table.existing_meters);
+
     simap_init(&ports);
     simap_put(&ports, "eth0", 5);
     simap_put(&ports, "eth1", 6);
@@ -1244,6 +1251,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
                 .aux = &ports,
                 .is_switch = true,
                 .group_table = &group_table,
+                .meter_table = &meter_table,
 
                 .pipeline = OVNACT_P_INGRESS,
                 .ingress_ptable = 8,
