diff --git a/lib/stopwatch-names.h b/lib/stopwatch-names.h
index 63a04db9ca..374fd29b16 100644
--- a/lib/stopwatch-names.h
+++ b/lib/stopwatch-names.h
@@ -34,5 +34,6 @@
 #define SYNC_METERS_RUN_STOPWATCH_NAME "sync_meters_run"
 #define LR_NAT_RUN_STOPWATCH_NAME "lr_nat_run"
 #define LR_STATEFUL_RUN_STOPWATCH_NAME "lr_stateful"
+#define LS_STATEFUL_RUN_STOPWATCH_NAME "ls_stateful"
 
 #endif
diff --git a/northd/automake.mk b/northd/automake.mk
index 3666cd452c..44140ce203 100644
--- a/northd/automake.mk
+++ b/northd/automake.mk
@@ -28,6 +28,8 @@ northd_ovn_northd_SOURCES = \
 	northd/en-lr-nat.h \
 	northd/en-lr-stateful.c \
 	northd/en-lr-stateful.h \
+	northd/en-ls-stateful.c \
+	northd/en-ls-stateful.h \
 	northd/inc-proc-northd.c \
 	northd/inc-proc-northd.h \
 	northd/ipam.c \
diff --git a/northd/en-lflow.c b/northd/en-lflow.c
index ef5aa11c17..83f11850ce 100644
--- a/northd/en-lflow.c
+++ b/northd/en-lflow.c
@@ -21,6 +21,7 @@
 #include "en-lflow.h"
 #include "en-lr-nat.h"
 #include "en-lr-stateful.h"
+#include "en-ls-stateful.h"
 #include "en-northd.h"
 #include "en-meters.h"
 
@@ -44,6 +45,8 @@ lflow_get_input_data(struct engine_node *node,
         engine_get_input_data("sync_meters", node);
     struct ed_type_lr_stateful *lr_sful_data =
         engine_get_input_data("lr_stateful", node);
+    struct ed_type_ls_stateful *ls_sful_data =
+        engine_get_input_data("ls_stateful", node);
 
     lflow_input->nbrec_bfd_table =
         EN_OVSDB_GET(engine_get_input("NB_bfd", node));
@@ -67,6 +70,7 @@ lflow_get_input_data(struct engine_node *node,
     lflow_input->lr_ports = &northd_data->lr_ports;
     lflow_input->ls_port_groups = &pg_data->ls_port_groups;
     lflow_input->lr_sful_table = &lr_sful_data->lr_sful_table;
+    lflow_input->ls_sful_table = &ls_sful_data->ls_sful_table;
     lflow_input->meter_groups = &sync_meters_data->meter_groups;
     lflow_input->lb_datapaths_map = &northd_data->lb_datapaths_map;
     lflow_input->svc_monitor_map = &northd_data->svc_monitor_map;
diff --git a/northd/en-lr-stateful.c b/northd/en-lr-stateful.c
index bd03c586da..a54749ad93 100644
--- a/northd/en-lr-stateful.c
+++ b/northd/en-lr-stateful.c
@@ -297,7 +297,9 @@ lr_stateful_lb_data_handler(struct engine_node *node, void *data_)
         /* For all the modified lr_stateful records (re)build the
          * vip nats. */
         HMAPX_FOR_EACH (hmapx_node, &data->trk_data.crupdated) {
-            lr_stateful_build_vip_nats(hmapx_node->data);
+            lr_sful_rec = hmapx_node->data;
+            lr_stateful_build_vip_nats(lr_sful_rec);
+            lr_sful_rec->has_lb_vip = od_has_lb_vip(lr_sful_rec->od);
         }
 
         engine_set_node_state(node, EN_UPDATED);
@@ -516,6 +518,8 @@ lr_stateful_record_init(struct lr_stateful_record *lr_sful_rec,
     if (!nbr->n_nat) {
         lr_stateful_build_vip_nats(lr_sful_rec);
     }
+
+    lr_sful_rec->has_lb_vip = od_has_lb_vip(lr_sful_rec->od);
 }
 
 static struct lr_stateful_input
diff --git a/northd/en-lr-stateful.h b/northd/en-lr-stateful.h
index 4360419788..0571e057f8 100644
--- a/northd/en-lr-stateful.h
+++ b/northd/en-lr-stateful.h
@@ -49,6 +49,8 @@ struct lr_stateful_record {
     const struct ovn_datapath *od;
     const struct lr_nat_record *lrnat_rec;
 
+    bool has_lb_vip;
+
     /* Load Balancer vIPs relevant for this datapath. */
     struct ovn_lb_ip_set *lb_ips;
 
diff --git a/northd/en-ls-stateful.c b/northd/en-ls-stateful.c
new file mode 100644
index 0000000000..117def531b
--- /dev/null
+++ b/northd/en-ls-stateful.c
@@ -0,0 +1,445 @@
+/*
+ * Copyright (c) 2023, Red Hat, Inc.
+ *
+ * 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.
+ */
+
+#include <config.h>
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+/* OVS includes */
+#include "include/openvswitch/hmap.h"
+#include "lib/bitmap.h"
+#include "lib/socket-util.h"
+#include "lib/uuidset.h"
+#include "openvswitch/util.h"
+#include "openvswitch/vlog.h"
+#include "stopwatch.h"
+
+/* OVN includes */
+#include "en-lb-data.h"
+#include "en-ls-stateful.h"
+#include "en-port-group.h"
+#include "lib/inc-proc-eng.h"
+#include "lib/lb.h"
+#include "lib/ovn-nb-idl.h"
+#include "lib/ovn-sb-idl.h"
+#include "lib/ovn-util.h"
+#include "lib/stopwatch-names.h"
+#include "northd.h"
+
+VLOG_DEFINE_THIS_MODULE(en_ls_stateful);
+
+/* Static function declarations. */
+static void ls_stateful_table_init(struct ls_stateful_table *);
+static void ls_stateful_table_clear(struct ls_stateful_table *);
+static void ls_stateful_table_destroy(struct ls_stateful_table *);
+static struct ls_stateful_record *ls_stateful_table_find_(
+    const struct ls_stateful_table *, const struct nbrec_logical_switch *);
+static void ls_stateful_table_build(struct ls_stateful_table *,
+                                  const struct ovn_datapaths *ls_datapaths,
+                                  const struct ls_port_group_table *);
+
+static struct ls_stateful_input ls_stateful_get_input_data(
+    struct engine_node *);
+
+static struct ls_stateful_record *ls_stateful_record_create(
+    struct ls_stateful_table *,
+    const struct ovn_datapath *,
+    const struct ls_port_group_table *);
+static void ls_stateful_record_destroy(struct ls_stateful_record *);
+static void ls_stateful_record_init(
+    struct ls_stateful_record *,
+    const struct ovn_datapath *,
+    const struct ls_port_group *,
+    const struct ls_port_group_table *);
+static void ls_stateful_record_reinit(
+    struct ls_stateful_record *,
+    const struct ls_port_group *,
+    const struct ls_port_group_table *);
+static bool ls_has_lb_vip(const struct ovn_datapath *);
+static void ls_stateful_record_set_acl_flags(struct ls_stateful_record *,
+                                           const struct ovn_datapath *,
+                                           const struct ls_port_group *,
+                                           const struct ls_port_group_table *);
+static bool ls_stateful_record_set_acl_flags_(struct ls_stateful_record *,
+                                            struct nbrec_acl **,
+                                            size_t n_acls);
+static struct ls_stateful_input ls_stateful_get_input_data(
+    struct engine_node *);
+
+struct ls_stateful_input {
+    const struct ls_port_group_table *ls_port_groups;
+    const struct ovn_datapaths *ls_datapaths;
+};
+
+/* public functions. */
+const struct ls_stateful_record *
+ls_stateful_table_find(
+    const struct ls_stateful_table *table,
+    const struct nbrec_logical_switch *nbs)
+{
+    return ls_stateful_table_find_(table, nbs);
+}
+
+void *
+en_ls_stateful_init(struct engine_node *node OVS_UNUSED,
+                  struct engine_arg *arg OVS_UNUSED)
+{
+    struct ed_type_ls_stateful *data = xzalloc(sizeof *data);
+    ls_stateful_table_init(&data->ls_sful_table);
+    hmapx_init(&data->trk_data.crupdated);
+    return data;
+}
+
+void
+en_ls_stateful_cleanup(void *data_)
+{
+    struct ed_type_ls_stateful *data =
+        (struct ed_type_ls_stateful *) data_;
+    ls_stateful_table_destroy(&data->ls_sful_table);
+    hmapx_destroy(&data->trk_data.crupdated);
+}
+
+void
+en_ls_stateful_clear_tracked_data(void *data_)
+{
+    struct ed_type_ls_stateful *data =
+        (struct ed_type_ls_stateful *) data_;
+
+    hmapx_clear(&data->trk_data.crupdated);
+}
+
+void
+en_ls_stateful_run(struct engine_node *node, void *data_)
+{
+    struct ls_stateful_input input_data = ls_stateful_get_input_data(node);
+    struct ed_type_ls_stateful *data = data_;
+
+    stopwatch_start(LS_STATEFUL_RUN_STOPWATCH_NAME, time_msec());
+
+    ls_stateful_table_clear(&data->ls_sful_table);
+    ls_stateful_table_build(&data->ls_sful_table, input_data.ls_datapaths,
+                          input_data.ls_port_groups);
+
+    stopwatch_stop(LS_STATEFUL_RUN_STOPWATCH_NAME, time_msec());
+    engine_set_node_state(node, EN_UPDATED);
+}
+
+/* Handler functions. */
+bool
+ls_stateful_northd_handler(struct engine_node *node, void *data_)
+{
+    struct northd_data *northd_data = engine_get_input_data("northd", node);
+    if (!northd_has_tracked_data(&northd_data->trk_data)) {
+        return false;
+    }
+
+    if (!northd_has_ls_lbs_in_tracked_data(&northd_data->trk_data) &&
+        !northd_has_ls_acls_in_tracked_data(&northd_data->trk_data)) {
+        return true;
+    }
+
+    struct northd_tracked_data *nd_changes = &northd_data->trk_data;
+    struct ls_stateful_input input_data = ls_stateful_get_input_data(node);
+    struct ls_stateful_record *ls_stateful_rec;
+    struct ed_type_ls_stateful *data = data_;
+    const struct ovn_datapath *od;
+    struct hmapx_node *hmapx_node;
+
+    struct hmapx changed_sful_od = HMAPX_INITIALIZER(&changed_sful_od);
+    HMAPX_FOR_EACH (hmapx_node, &nd_changes->ls_with_changed_lbs) {
+        hmapx_add(&changed_sful_od, hmapx_node->data);
+    }
+
+    HMAPX_FOR_EACH (hmapx_node, &nd_changes->ls_with_changed_acls) {
+        hmapx_add(&changed_sful_od, hmapx_node->data);
+    }
+
+    HMAPX_FOR_EACH (hmapx_node, &changed_sful_od) {
+        od = hmapx_node->data;
+
+        ls_stateful_rec = ls_stateful_table_find_(&data->ls_sful_table,
+                                                  od->nbs);
+        ovs_assert(ls_stateful_rec);
+        ls_stateful_record_reinit(ls_stateful_rec, NULL,
+                                  input_data.ls_port_groups);
+
+        /* Add the ls_stateful_rec to the tracking data. */
+        hmapx_add(&data->trk_data.crupdated, ls_stateful_rec);
+    }
+
+    if (ls_stateful_has_tracked_data(&data->trk_data)) {
+        engine_set_node_state(node, EN_UPDATED);
+    }
+
+    hmapx_destroy(&changed_sful_od);
+
+    return true;
+}
+
+bool
+ls_stateful_port_group_handler(struct engine_node *node, void *data_)
+{
+    struct port_group_data *pg_data =
+        engine_get_input_data("port_group", node);
+
+    if (pg_data->ls_port_groups_sets_changed) {
+        return false;
+    }
+
+    /* port_group engine node doesn't provide the tracking data yet.
+     * Loop through all the ls port groups and update the ls_stateful_rec.
+     * This is still better than returning false. */
+    struct ls_stateful_input input_data = ls_stateful_get_input_data(node);
+    struct ed_type_ls_stateful *data = data_;
+    const struct ls_port_group *ls_pg;
+
+    LS_PORT_GROUP_TABLE_FOR_EACH (ls_pg, input_data.ls_port_groups) {
+        struct ls_stateful_record *ls_sful_rec =
+            ls_stateful_table_find_(&data->ls_sful_table, ls_pg->nbs);
+        ovs_assert(ls_sful_rec);
+
+        bool had_stateful_acl = ls_sful_rec->has_stateful_acl;
+        uint64_t max_acl_tier = ls_sful_rec->max_acl_tier;
+        bool had_acls = ls_sful_rec->has_acls;
+        bool modified = false;
+
+        ls_stateful_record_reinit(ls_sful_rec, ls_pg,
+                                  input_data.ls_port_groups);
+
+        if ((had_stateful_acl != ls_sful_rec->has_stateful_acl)
+            || (had_acls != ls_sful_rec->has_acls)
+            || max_acl_tier != ls_sful_rec->max_acl_tier) {
+            modified = true;
+        }
+
+        if (modified) {
+            /* Add the ls_sful_rec to the tracking data. */
+            hmapx_add(&data->trk_data.crupdated, ls_sful_rec);
+        }
+    }
+
+    if (ls_stateful_has_tracked_data(&data->trk_data)) {
+        engine_set_node_state(node, EN_UPDATED);
+    }
+    return true;
+}
+
+/* static functions. */
+static void
+ls_stateful_table_init(struct ls_stateful_table *table)
+{
+    *table = (struct ls_stateful_table) {
+        .entries = HMAP_INITIALIZER(&table->entries),
+    };
+}
+
+static void
+ls_stateful_table_destroy(struct ls_stateful_table *table)
+{
+    ls_stateful_table_clear(table);
+    hmap_destroy(&table->entries);
+}
+
+static void
+ls_stateful_table_clear(struct ls_stateful_table *table)
+{
+    struct ls_stateful_record *ls_stateful_rec;
+    HMAP_FOR_EACH_POP (ls_stateful_rec, key_node, &table->entries) {
+        ls_stateful_record_destroy(ls_stateful_rec);
+    }
+}
+
+static void
+ls_stateful_table_build(struct ls_stateful_table *table,
+                      const struct ovn_datapaths *ls_datapaths,
+                      const struct ls_port_group_table *ls_pgs)
+{
+    const struct ovn_datapath *od;
+    HMAP_FOR_EACH (od, key_node, &ls_datapaths->datapaths) {
+        ls_stateful_record_create(table, od, ls_pgs);
+    }
+}
+
+struct ls_stateful_record *
+ls_stateful_table_find_(const struct ls_stateful_table *table,
+                      const struct nbrec_logical_switch *nbs)
+{
+    struct ls_stateful_record *rec;
+
+    HMAP_FOR_EACH_WITH_HASH (rec, key_node,
+                             uuid_hash(&nbs->header_.uuid), &table->entries) {
+        if (nbs == rec->od->nbs) {
+            return rec;
+        }
+    }
+    return NULL;
+}
+
+static struct ls_stateful_record *
+ls_stateful_record_create(struct ls_stateful_table *table,
+                        const struct ovn_datapath *od,
+                        const struct ls_port_group_table *ls_pgs)
+{
+    struct ls_stateful_record *ls_sful_rec = xzalloc(sizeof *ls_sful_rec);
+    ls_sful_rec->od = od;
+    ls_stateful_record_init(ls_sful_rec, od, NULL, ls_pgs);
+
+    hmap_insert(&table->entries, &ls_sful_rec->key_node,
+                uuid_hash(&ls_sful_rec->od->nbs->header_.uuid));
+
+    return ls_sful_rec;
+}
+
+static void
+ls_stateful_record_destroy(struct ls_stateful_record *ls_sful_rec)
+{
+    free(ls_sful_rec);
+}
+
+static void
+ls_stateful_record_init(struct ls_stateful_record *ls_sful_rec,
+                      const struct ovn_datapath *od,
+                      const struct ls_port_group *ls_pg,
+                      const struct ls_port_group_table *ls_pgs)
+{
+    ls_sful_rec->has_lb_vip = ls_has_lb_vip(od);
+    ls_stateful_record_set_acl_flags(ls_sful_rec, od, ls_pg, ls_pgs);
+}
+
+static void
+ls_stateful_record_reinit(struct ls_stateful_record *ls_sful_rec,
+                        const struct ls_port_group *ls_pg,
+                        const struct ls_port_group_table *ls_pgs)
+{
+    ls_stateful_record_init(ls_sful_rec, ls_sful_rec->od, ls_pg, ls_pgs);
+}
+
+static bool
+lb_has_vip(const struct nbrec_load_balancer *lb)
+{
+    return !smap_is_empty(&lb->vips);
+}
+
+static bool
+lb_group_has_vip(const struct nbrec_load_balancer_group *lb_group)
+{
+    for (size_t i = 0; i < lb_group->n_load_balancer; i++) {
+        if (lb_has_vip(lb_group->load_balancer[i])) {
+            return true;
+        }
+    }
+    return false;
+}
+
+static bool
+ls_has_lb_vip(const struct ovn_datapath *od)
+{
+    for (size_t i = 0; i < od->nbs->n_load_balancer; i++) {
+        if (lb_has_vip(od->nbs->load_balancer[i])) {
+            return true;
+        }
+    }
+
+    for (size_t i = 0; i < od->nbs->n_load_balancer_group; i++) {
+        if (lb_group_has_vip(od->nbs->load_balancer_group[i])) {
+            return true;
+        }
+    }
+    return false;
+}
+
+static void
+ls_stateful_record_set_acl_flags(struct ls_stateful_record *ls_sful_rec,
+                               const struct ovn_datapath *od,
+                               const struct ls_port_group *ls_pg,
+                               const struct ls_port_group_table *ls_pgs)
+{
+    ls_sful_rec->has_stateful_acl = false;
+    ls_sful_rec->max_acl_tier = 0;
+    ls_sful_rec->has_acls = false;
+
+    if (ls_stateful_record_set_acl_flags_(ls_sful_rec, od->nbs->acls,
+                                        od->nbs->n_acls)) {
+        return;
+    }
+
+    if (!ls_pg) {
+        ls_pg = ls_port_group_table_find(ls_pgs, od->nbs);
+    }
+
+    if (!ls_pg) {
+        return;
+    }
+
+    const struct ls_port_group_record *ls_pg_rec;
+    HMAP_FOR_EACH (ls_pg_rec, key_node, &ls_pg->nb_pgs) {
+        if (ls_stateful_record_set_acl_flags_(ls_sful_rec,
+                                            ls_pg_rec->nb_pg->acls,
+                                            ls_pg_rec->nb_pg->n_acls)) {
+            return;
+        }
+    }
+}
+
+static bool
+ls_stateful_record_set_acl_flags_(struct ls_stateful_record *ls_sful_rec,
+                                struct nbrec_acl **acls,
+                                size_t n_acls)
+{
+    /* A true return indicates that there are no possible ACL flags
+     * left to set on ls_stateful record. A false return indicates that
+     * further ACLs should be explored in case more flags need to be
+     * set on ls_stateful record.
+     */
+    if (!n_acls) {
+        return false;
+    }
+
+    ls_sful_rec->has_acls = true;
+    for (size_t i = 0; i < n_acls; i++) {
+        const struct nbrec_acl *acl = acls[i];
+        if (acl->tier > ls_sful_rec->max_acl_tier) {
+            ls_sful_rec->max_acl_tier = acl->tier;
+        }
+        if (!ls_sful_rec->has_stateful_acl
+                && !strcmp(acl->action, "allow-related")) {
+            ls_sful_rec->has_stateful_acl = true;
+        }
+        if (ls_sful_rec->has_stateful_acl &&
+            ls_sful_rec->max_acl_tier ==
+                nbrec_acl_col_tier.type.value.integer.max) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static struct ls_stateful_input
+ls_stateful_get_input_data(struct engine_node *node)
+{
+    const struct northd_data *northd_data =
+        engine_get_input_data("northd", node);
+    const struct port_group_data *pg_data =
+        engine_get_input_data("port_group", node);
+
+    return (struct ls_stateful_input) {
+        .ls_port_groups = &pg_data->ls_port_groups,
+        .ls_datapaths = &northd_data->ls_datapaths,
+    };
+}
diff --git a/northd/en-ls-stateful.h b/northd/en-ls-stateful.h
new file mode 100644
index 0000000000..e409bb0141
--- /dev/null
+++ b/northd/en-ls-stateful.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2023, Red Hat, Inc.
+ *
+ * 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.
+ */
+#ifndef EN_LS_STATEFUL_H
+#define EN_LS_STATEFUL_H 1
+
+#include <stdint.h>
+
+/* OVS includes. */
+#include "lib/hmapx.h"
+#include "openvswitch/hmap.h"
+#include "sset.h"
+
+/* OVN includes. */
+#include "lib/inc-proc-eng.h"
+#include "lib/lb.h"
+#include "lib/ovn-nb-idl.h"
+#include "lib/ovn-sb-idl.h"
+#include "lib/ovn-util.h"
+#include "lib/stopwatch-names.h"
+
+struct ls_stateful_record {
+    struct hmap_node key_node;
+
+    const struct ovn_datapath *od;
+    bool has_stateful_acl;
+    bool has_lb_vip;
+    bool has_acls;
+    uint64_t max_acl_tier;
+};
+
+struct ls_stateful_table {
+    struct hmap entries;
+};
+
+#define LS_STATEFUL_TABLE_FOR_EACH(LS_STATEFUL_REC, TABLE) \
+    HMAP_FOR_EACH (LS_STATEFUL_REC, key_node, &(TABLE)->entries)
+
+#define LS_STATEFUL_TABLE_FOR_EACH_IN_P(LS_STATEFUL_REC, JOBID, TABLE) \
+    HMAP_FOR_EACH_IN_PARALLEL (LS_STATEFUL_REC, key_node, JOBID, \
+                               &(TABLE)->entries)
+
+struct ls_stateful_tracked_data {
+    /* Created or updated logical switch with LB and ACL data. */
+    struct hmapx crupdated; /* Stores 'struct ls_stateful_record'. */
+};
+
+struct ed_type_ls_stateful {
+    struct ls_stateful_table ls_sful_table;
+    struct ls_stateful_tracked_data trk_data;
+};
+
+void *en_ls_stateful_init(struct engine_node *, struct engine_arg *);
+void en_ls_stateful_cleanup(void *data);
+void en_ls_stateful_clear_tracked_data(void *data);
+void en_ls_stateful_run(struct engine_node *, void *data);
+
+bool ls_stateful_northd_handler(struct engine_node *, void *data);
+bool ls_stateful_port_group_handler(struct engine_node *, void *data);
+
+const struct ls_stateful_record *ls_stateful_table_find(
+    const struct ls_stateful_table *, const struct nbrec_logical_switch *);
+
+static inline bool
+ls_stateful_has_tracked_data(struct ls_stateful_tracked_data *trk_data) {
+    return !hmapx_is_empty(&trk_data->crupdated);
+}
+
+#endif /* EN_LS_STATEFUL_H */
diff --git a/northd/en-northd.c b/northd/en-northd.c
index 546397f3dc..5143603f39 100644
--- a/northd/en-northd.c
+++ b/northd/en-northd.c
@@ -171,7 +171,7 @@ northd_nb_logical_switch_handler(struct engine_node *node,
         return false;
     }
 
-    if (northd_has_lsps_in_tracked_data(&nd->trk_data)) {
+    if (northd_has_tracked_data(&nd->trk_data)) {
         engine_set_node_state(node, EN_UPDATED);
     }
 
diff --git a/northd/en-port-group.h b/northd/en-port-group.h
index 3b28a23694..54014062ce 100644
--- a/northd/en-port-group.h
+++ b/northd/en-port-group.h
@@ -48,6 +48,9 @@ struct ls_port_group_record {
     struct sset ports;          /* Subset of 'nb_pg' ports in this record. */
 };
 
+#define LS_PORT_GROUP_TABLE_FOR_EACH(LS_PG, TABLE) \
+    HMAP_FOR_EACH (LS_PG, key_node, &(TABLE)->entries)
+
 void ls_port_group_table_init(struct ls_port_group_table *);
 void ls_port_group_table_clear(struct ls_port_group_table *);
 void ls_port_group_table_destroy(struct ls_port_group_table *);
diff --git a/northd/inc-proc-northd.c b/northd/inc-proc-northd.c
index adb38dde78..9ce4279ee8 100644
--- a/northd/inc-proc-northd.c
+++ b/northd/inc-proc-northd.c
@@ -33,6 +33,7 @@
 #include "en-lb-data.h"
 #include "en-lr-stateful.h"
 #include "en-lr-nat.h"
+#include "en-ls-stateful.h"
 #include "en-northd.h"
 #include "en-lflow.h"
 #include "en-northd-output.h"
@@ -150,6 +151,7 @@ static ENGINE_NODE(sync_to_sb_pb, "sync_to_sb_pb");
 static ENGINE_NODE_WITH_CLEAR_TRACK_DATA(lb_data, "lb_data");
 static ENGINE_NODE_WITH_CLEAR_TRACK_DATA(lr_nat, "lr_nat");
 static ENGINE_NODE_WITH_CLEAR_TRACK_DATA(lr_stateful, "lr_stateful");
+static ENGINE_NODE_WITH_CLEAR_TRACK_DATA(ls_stateful, "ls_stateful");
 
 void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
                           struct ovsdb_idl_loop *sb)
@@ -200,6 +202,10 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_lr_stateful, &en_lb_data,
                      lr_stateful_lb_data_handler);
 
+    engine_add_input(&en_ls_stateful, &en_northd, ls_stateful_northd_handler);
+    engine_add_input(&en_ls_stateful, &en_port_group,
+                     ls_stateful_port_group_handler);
+
     engine_add_input(&en_mac_binding_aging, &en_nb_nb_global, NULL);
     engine_add_input(&en_mac_binding_aging, &en_sb_mac_binding, NULL);
     engine_add_input(&en_mac_binding_aging, &en_northd, NULL);
@@ -222,6 +228,7 @@ void inc_proc_northd_init(struct ovsdb_idl_loop *nb,
     engine_add_input(&en_lflow, &en_sb_multicast_group, NULL);
     engine_add_input(&en_lflow, &en_sb_igmp_group, NULL);
     engine_add_input(&en_lflow, &en_lr_stateful, NULL);
+    engine_add_input(&en_lflow, &en_ls_stateful, NULL);
     engine_add_input(&en_lflow, &en_northd, lflow_northd_handler);
     engine_add_input(&en_lflow, &en_port_group, lflow_port_group_handler);
 
diff --git a/northd/northd.c b/northd/northd.c
index 176433a6c5..c8e2bd4790 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -45,6 +45,7 @@
 #include "en-lb-data.h"
 #include "en-lr-nat.h"
 #include "en-lr-stateful.h"
+#include "en-ls-stateful.h"
 #include "lib/ovn-parallel-hmap.h"
 #include "ovn/actions.h"
 #include "ovn/features.h"
@@ -575,7 +576,7 @@ lb_group_has_vip(const struct nbrec_load_balancer_group *lb_group)
 }
 
 static bool
-ls_has_lb_vip(struct ovn_datapath *od)
+ls_has_lb_vip(const struct ovn_datapath *od)
 {
     for (size_t i = 0; i < od->nbs->n_load_balancer; i++) {
         if (lb_has_vip(od->nbs->load_balancer[i])) {
@@ -592,7 +593,7 @@ ls_has_lb_vip(struct ovn_datapath *od)
 }
 
 static bool
-lr_has_lb_vip(struct ovn_datapath *od)
+lr_has_lb_vip(const struct ovn_datapath *od)
 {
     for (size_t i = 0; i < od->nbr->n_load_balancer; i++) {
         if (lb_has_vip(od->nbr->load_balancer[i])) {
@@ -608,13 +609,13 @@ lr_has_lb_vip(struct ovn_datapath *od)
     return false;
 }
 
-static void
-init_lb_for_datapath(struct ovn_datapath *od)
+bool
+od_has_lb_vip(const struct ovn_datapath *od)
 {
     if (od->nbs) {
-        od->has_lb_vip = ls_has_lb_vip(od);
+        return ls_has_lb_vip(od);
     } else {
-        od->has_lb_vip = lr_has_lb_vip(od);
+        return lr_has_lb_vip(od);
     }
 }
 
@@ -1058,7 +1059,6 @@ join_datapaths(const struct nbrec_logical_switch_table *nbrec_ls_table,
 
         init_ipam_info_for_datapath(od);
         init_mcast_info_for_datapath(od);
-        init_lb_for_datapath(od);
     }
 
     const struct nbrec_logical_router *nbr;
@@ -1089,7 +1089,6 @@ join_datapaths(const struct nbrec_logical_switch_table *nbrec_ls_table,
             ovs_list_push_back(nb_only, &od->list);
         }
         init_mcast_info_for_datapath(od);
-        init_lb_for_datapath(od);
         if (smap_get(&od->nbr->options, "chassis")) {
             od->is_gw_router = true;
         }
@@ -2570,7 +2569,8 @@ get_nat_addresses(const struct ovn_port *op, size_t *n, bool routable_only,
     size_t n_nats = 0;
     struct eth_addr mac;
     if (!op || !op->nbrp || !op->od || !op->od->nbr
-        || (!op->od->nbr->n_nat && !op->od->has_lb_vip)
+        || (!op->od->nbr->n_nat && (!lr_sful_rec
+                                    || !lr_sful_rec->has_lb_vip))
         || !eth_addr_from_string(op->nbrp->mac, &mac)) {
         *n = n_nats;
         return NULL;
@@ -3817,7 +3817,7 @@ build_lrouter_lbs_check(const struct ovn_datapaths *lr_datapaths)
     HMAP_FOR_EACH (od, key_node, &lr_datapaths->datapaths) {
         ovs_assert(od->nbr);
 
-        if (od->has_lb_vip && od->n_l3dgw_ports > 1
+        if (od_has_lb_vip(od) && od->n_l3dgw_ports > 1
                 && !smap_get(&od->nbr->options, "chassis")) {
             static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
             VLOG_WARN_RL(&rl, "Load-balancers are configured on logical "
@@ -4737,6 +4737,8 @@ destroy_northd_data_tracked_changes(struct northd_data *nd)
     destroy_tracked_ovn_ports(&trk_changes->trk_lsps);
     destroy_tracked_lbs(&trk_changes->trk_lbs);
     hmapx_clear(&trk_changes->lr_with_changed_nats);
+    hmapx_clear(&trk_changes->ls_with_changed_lbs);
+    hmapx_clear(&trk_changes->ls_with_changed_acls);
     nd->trk_data.type = NORTHD_TRACKED_NONE;
 }
 
@@ -4751,6 +4753,8 @@ init_northd_tracked_data(struct northd_data *nd)
     hmapx_init(&trk_data->trk_lbs.crupdated);
     hmapx_init(&trk_data->trk_lbs.deleted);
     hmapx_init(&trk_data->lr_with_changed_nats);
+    hmapx_init(&trk_data->ls_with_changed_lbs);
+    hmapx_init(&trk_data->ls_with_changed_acls);
 }
 
 static void
@@ -4764,6 +4768,8 @@ destroy_northd_tracked_data(struct northd_data *nd)
     hmapx_destroy(&trk_data->trk_lbs.crupdated);
     hmapx_destroy(&trk_data->trk_lbs.deleted);
     hmapx_destroy(&trk_data->lr_with_changed_nats);
+    hmapx_destroy(&trk_data->ls_with_changed_lbs);
+    hmapx_destroy(&trk_data->ls_with_changed_acls);
 }
 
 /* Check if a changed LSP can be handled incrementally within the I-P engine
@@ -4881,6 +4887,7 @@ ls_port_create(struct ovsdb_idl_txn *ovnsb_txn, struct hmap *ls_ports,
  *    - logical switch ports.
  *    - load balancers.
  *    - load balancer groups.
+ *    - ACLs
  */
 static bool
 ls_changes_can_be_handled(
@@ -5105,6 +5112,25 @@ fail:
     return false;
 }
 
+static bool
+is_acls_seqno_changed(struct nbrec_acl **nb_acls, size_t n_nb_acls)
+{
+    for (size_t i = 0; i < n_nb_acls; i++) {
+        if (nbrec_acl_row_get_seqno(nb_acls[i],
+                                    OVSDB_IDL_CHANGE_MODIFY) > 0) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static bool
+is_ls_acls_changed(const struct nbrec_logical_switch *nbs) {
+    return (nbrec_logical_switch_is_updated(nbs, NBREC_LOGICAL_SWITCH_COL_ACLS)
+            || is_acls_seqno_changed(nbs->acls, nbs->n_acls));
+}
+
 /* Return true if changes are handled incrementally, false otherwise.
  * When there are any changes, try to track what's exactly changed and set
  * northd_data->trk_data accordingly.
@@ -5146,6 +5172,10 @@ northd_handle_ls_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
                                    ni, nd, od, &trk_data->trk_lsps)) {
             goto fail;
         }
+
+        if (is_ls_acls_changed(changed_ls)) {
+            hmapx_add(&trk_data->ls_with_changed_acls, od);
+        }
     }
 
     if (!hmapx_is_empty(&trk_data->trk_lsps.created)
@@ -5154,6 +5184,10 @@ northd_handle_ls_changes(struct ovsdb_idl_txn *ovnsb_idl_txn,
         trk_data->type |= NORTHD_TRACKED_PORTS;
     }
 
+    if (!hmapx_is_empty(&trk_data->ls_with_changed_acls)) {
+        trk_data->type |= NORTHD_TRACKED_LS_ACLS;
+    }
+
     return true;
 
 fail:
@@ -5445,7 +5479,9 @@ northd_handle_lb_data_changes(struct tracked_lb_data *trk_lb_data,
         BITMAP_FOR_EACH_1 (index, ods_size(ls_datapaths),
                            lb_dps->nb_ls_map) {
             od = ls_datapaths->array[index];
-            init_lb_for_datapath(od);
+
+            /* Add the ls datapath to the northd tracked data. */
+            hmapx_add(&nd_changes->ls_with_changed_lbs, od);
         }
 
         hmap_remove(lb_datapaths_map, &lb_dps->hmap_node);
@@ -5525,8 +5561,8 @@ northd_handle_lb_data_changes(struct tracked_lb_data *trk_lb_data,
             }
         }
 
-        /* Re-evaluate 'od->has_lb_vip' */
-        init_lb_for_datapath(od);
+        /* Add the ls datapath to the northd tracked data. */
+        hmapx_add(&nd_changes->ls_with_changed_lbs, od);
     }
 
     LIST_FOR_EACH (codlb, list_node, &trk_lb_data->crupdated_lr_lbs) {
@@ -5561,9 +5597,6 @@ northd_handle_lb_data_changes(struct tracked_lb_data *trk_lb_data,
                 hmapx_add(&nd_changes->trk_lbs.crupdated, lb_dps);
             }
         }
-
-        /* Re-evaluate 'od->has_lb_vip' */
-        init_lb_for_datapath(od);
     }
 
     HMAP_FOR_EACH (clb, hmap_node, &trk_lb_data->crupdated_lbs) {
@@ -5576,15 +5609,9 @@ northd_handle_lb_data_changes(struct tracked_lb_data *trk_lb_data,
         BITMAP_FOR_EACH_1 (index, ods_size(ls_datapaths),
                            lb_dps->nb_ls_map) {
             od = ls_datapaths->array[index];
-            /* Re-evaluate 'od->has_lb_vip' */
-            init_lb_for_datapath(od);
-        }
 
-        BITMAP_FOR_EACH_1 (index, ods_size(lr_datapaths),
-                           lb_dps->nb_lr_map) {
-            od = lr_datapaths->array[index];
-            /* Re-evaluate 'od->has_lb_vip' */
-            init_lb_for_datapath(od);
+            /* Add the ls datapath to the northd tracked data. */
+            hmapx_add(&nd_changes->ls_with_changed_lbs, od);
         }
     }
 
@@ -5606,17 +5633,14 @@ northd_handle_lb_data_changes(struct tracked_lb_data *trk_lb_data,
             for (size_t i = 0; i < lbgrp_dps->n_lr; i++) {
                 od = lbgrp_dps->lr[i];
                 ovn_lb_datapaths_add_lr(lb_dps, 1, &od);
-
-                /* Re-evaluate 'od->has_lb_vip' */
-                init_lb_for_datapath(od);
             }
 
             for (size_t i = 0; i < lbgrp_dps->n_ls; i++) {
                od = lbgrp_dps->ls[i];
                 ovn_lb_datapaths_add_ls(lb_dps, 1, &od);
 
-                /* Re-evaluate 'od->has_lb_vip' */
-                init_lb_for_datapath(od);
+                /* Add the ls datapath to the northd tracked data. */
+                hmapx_add(&nd_changes->ls_with_changed_lbs, od);
             }
 
             /* Add the lb to the northd tracked data. */
@@ -5629,6 +5653,10 @@ northd_handle_lb_data_changes(struct tracked_lb_data *trk_lb_data,
         nd_changes->type |= NORTHD_TRACKED_LBS;
     }
 
+    if (!hmapx_is_empty(&nd_changes->ls_with_changed_lbs)) {
+        nd_changes->type |= NORTHD_TRACKED_LS_LBS;
+    }
+
     return true;
 }
 
@@ -6562,63 +6590,6 @@ build_dhcpv6_action(struct ovn_port *op, struct in6_addr *offer_ip,
     return true;
 }
 
-static bool
-od_set_acl_flags(struct ovn_datapath *od, struct nbrec_acl **acls,
-                 size_t n_acls)
-{
-    /* A true return indicates that there are no possible ACL flags
-     * left to set on od. A false return indicates that further ACLs
-     * should be explored in case more flags need to be set on od
-     */
-    if (!n_acls) {
-        return false;
-    }
-
-    od->has_acls = true;
-    for (size_t i = 0; i < n_acls; i++) {
-        const struct nbrec_acl *acl = acls[i];
-        if (acl->tier > od->max_acl_tier) {
-            od->max_acl_tier = acl->tier;
-        }
-        if (!od->has_stateful_acl && !strcmp(acl->action, "allow-related")) {
-            od->has_stateful_acl = true;
-        }
-        if (od->has_stateful_acl &&
-            od->max_acl_tier == nbrec_acl_col_tier.type.value.integer.max) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
-static void
-ls_get_acl_flags(struct ovn_datapath *od,
-                 const struct ls_port_group_table *ls_port_groups)
-{
-    od->has_acls = false;
-    od->has_stateful_acl = false;
-    od->max_acl_tier = 0;
-
-    if (od_set_acl_flags(od, od->nbs->acls, od->nbs->n_acls)) {
-        return;
-    }
-
-    const struct ls_port_group *ls_pg =
-        ls_port_group_table_find(ls_port_groups, od->nbs);
-    if (!ls_pg) {
-        return;
-    }
-
-    const struct ls_port_group_record *ls_pg_rec;
-    HMAP_FOR_EACH (ls_pg_rec, key_node, &ls_pg->nb_pgs) {
-        if (od_set_acl_flags(od, ls_pg_rec->nb_pg->acls,
-                             ls_pg_rec->nb_pg->n_acls)) {
-            return;
-        }
-    }
-}
-
 /* Adds the logical flows in the (in/out) check port sec stage only if
  *   - the lport is disabled or
  *   - lport is of type vtep - to skip the ingress pipeline.
@@ -6766,9 +6737,10 @@ build_lswitch_output_port_sec_od(struct ovn_datapath *od,
 }
 
 static void
-skip_port_from_conntrack(struct ovn_datapath *od, struct ovn_port *op,
-                         enum ovn_stage in_stage, enum ovn_stage out_stage,
-                         uint16_t priority, struct hmap *lflows)
+skip_port_from_conntrack(const struct ovn_datapath *od, struct ovn_port *op,
+                         bool has_stateful_acl, enum ovn_stage in_stage,
+                         enum ovn_stage out_stage, uint16_t priority,
+                         struct hmap *lflows)
 {
     /* Can't use ct() for router ports. Consider the following configuration:
      * lp1(10.0.0.2) on hostA--ls1--lr0--ls2--lp2(10.0.1.2) on hostB, For a
@@ -6781,7 +6753,7 @@ skip_port_from_conntrack(struct ovn_datapath *od, struct ovn_port *op,
      * conntrack state across all chassis. */
 
     const char *ingress_action = "next;";
-    const char *egress_action = od->has_stateful_acl
+    const char *egress_action = has_stateful_acl
                                 ? "next;"
                                 : "ct_clear; next;";
 
@@ -6800,7 +6772,7 @@ skip_port_from_conntrack(struct ovn_datapath *od, struct ovn_port *op,
 }
 
 static void
-build_stateless_filter(struct ovn_datapath *od,
+build_stateless_filter(const struct ovn_datapath *od,
                        const struct nbrec_acl *acl,
                        struct hmap *lflows)
 {
@@ -6821,7 +6793,7 @@ build_stateless_filter(struct ovn_datapath *od,
 }
 
 static void
-build_stateless_filters(struct ovn_datapath *od,
+build_stateless_filters(const struct ovn_datapath *od,
                         const struct ls_port_group_table *ls_port_groups,
                         struct hmap *lflows)
 {
@@ -6851,9 +6823,7 @@ build_stateless_filters(struct ovn_datapath *od,
 }
 
 static void
-build_pre_acls(struct ovn_datapath *od,
-               const struct ls_port_group_table *ls_port_groups,
-               struct hmap *lflows)
+build_pre_acls(struct ovn_datapath *od, struct hmap *lflows)
 {
     /* Ingress and Egress Pre-ACL Table (Priority 0): Packets are
      * allowed by default. */
@@ -6865,18 +6835,26 @@ build_pre_acls(struct ovn_datapath *od,
 
     ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 110,
                   "eth.src == $svc_monitor_mac", "next;");
+}
+
+static void
+build_ls_stateful_rec_pre_acls(const struct ls_stateful_record *ls_sful_rec,
+                             const struct ls_port_group_table *ls_port_groups,
+                             struct hmap *lflows)
+{
+    const struct ovn_datapath *od = ls_sful_rec->od;
 
     /* If there are any stateful ACL rules in this datapath, we may
      * send IP packets for some (allow) filters through the conntrack action,
      * which handles defragmentation, in order to match L4 headers. */
-    if (od->has_stateful_acl) {
+    if (ls_sful_rec->has_stateful_acl) {
         for (size_t i = 0; i < od->n_router_ports; i++) {
-            skip_port_from_conntrack(od, od->router_ports[i],
+            skip_port_from_conntrack(od, od->router_ports[i], true,
                                      S_SWITCH_IN_PRE_ACL, S_SWITCH_OUT_PRE_ACL,
                                      110, lflows);
         }
         for (size_t i = 0; i < od->n_localnet_ports; i++) {
-            skip_port_from_conntrack(od, od->localnet_ports[i],
+            skip_port_from_conntrack(od, od->localnet_ports[i], true,
                                      S_SWITCH_IN_PRE_ACL,
                                      S_SWITCH_OUT_PRE_ACL,
                                      110, lflows);
@@ -6914,7 +6892,7 @@ build_pre_acls(struct ovn_datapath *od,
                       REGBIT_CONNTRACK_DEFRAG" = 1; next;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_ACL, 100, "ip",
                       REGBIT_CONNTRACK_DEFRAG" = 1; next;");
-    } else if (od->has_lb_vip) {
+    } else if (ls_sful_rec->has_lb_vip) {
         /* We'll build stateless filters if there are LB rules so that
          * the stateless flows are not tracked in pre-lb. */
          build_stateless_filters(od, ls_port_groups, lflows);
@@ -7042,30 +7020,40 @@ build_pre_lb(struct ovn_datapath *od, const struct shash *meter_groups,
     ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB, 0, "1", "next;");
     ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB, 0, "1", "next;");
 
+    /* Do not send statless flows via conntrack */
+    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB, 110,
+                  REGBIT_ACL_STATELESS" == 1", "next;");
+    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB, 110,
+                  REGBIT_ACL_STATELESS" == 1", "next;");
+}
+
+static void
+build_ls_stateful_rec_pre_lb(const struct ls_stateful_record *ls_stateful_rec,
+                             struct hmap *lflows)
+{
+    const struct ovn_datapath *od = ls_stateful_rec->od;
+
     for (size_t i = 0; i < od->n_router_ports; i++) {
         skip_port_from_conntrack(od, od->router_ports[i],
+                                 ls_stateful_rec->has_stateful_acl,
                                  S_SWITCH_IN_PRE_LB, S_SWITCH_OUT_PRE_LB,
                                  110, lflows);
     }
+
     /* Localnet ports have no need for going through conntrack, unless
      * the logical switch has a load balancer. Then, conntrack is necessary
      * so that traffic arriving via the localnet port can be load
      * balanced.
      */
-    if (!od->has_lb_vip) {
+    if (!ls_stateful_rec->has_lb_vip) {
         for (size_t i = 0; i < od->n_localnet_ports; i++) {
             skip_port_from_conntrack(od, od->localnet_ports[i],
+                                     ls_stateful_rec->has_stateful_acl,
                                      S_SWITCH_IN_PRE_LB, S_SWITCH_OUT_PRE_LB,
                                      110, lflows);
         }
     }
 
-    /* Do not sent statless flows via conntrack */
-    ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB, 110,
-                  REGBIT_ACL_STATELESS" == 1", "next;");
-    ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB, 110,
-                  REGBIT_ACL_STATELESS" == 1", "next;");
-
     /* 'REGBIT_CONNTRACK_NAT' is set to let the pre-stateful table send
      * packet to conntrack for defragmentation and possibly for unNATting.
      *
@@ -7096,7 +7084,7 @@ build_pre_lb(struct ovn_datapath *od, const struct shash *meter_groups,
      * ingress pipeline if a load balancer is configured. We can now
      * add a lflow to drop ct.inv packets.
      */
-    if (od->has_lb_vip) {
+    if (ls_stateful_rec->has_lb_vip) {
         ovn_lflow_add(lflows, od, S_SWITCH_IN_PRE_LB,
                       100, "ip", REGBIT_CONNTRACK_NAT" = 1; next;");
         ovn_lflow_add(lflows, od, S_SWITCH_OUT_PRE_LB,
@@ -7137,10 +7125,12 @@ build_pre_stateful(struct ovn_datapath *od,
 }
 
 static void
-build_acl_hints(struct ovn_datapath *od,
+build_acl_hints(const struct ls_stateful_record *ls_sful_rec,
                 const struct chassis_features *features,
                 struct hmap *lflows)
 {
+    const struct ovn_datapath *od = ls_sful_rec->od;
+
     /* This stage builds hints for the IN/OUT_ACL stage. Based on various
      * combinations of ct flags packets may hit only a subset of the logical
      * flows in the IN/OUT_ACL stage.
@@ -7164,13 +7154,13 @@ build_acl_hints(struct ovn_datapath *od,
         const char *match;
 
         /* In any case, advance to the next stage. */
-        if (!od->has_acls && !od->has_lb_vip) {
+        if (!ls_sful_rec->has_acls && !ls_sful_rec->has_lb_vip) {
             ovn_lflow_add(lflows, od, stage, UINT16_MAX, "1", "next;");
         } else {
             ovn_lflow_add(lflows, od, stage, 0, "1", "next;");
         }
 
-        if (!od->has_stateful_acl && !od->has_lb_vip) {
+        if (!ls_sful_rec->has_stateful_acl && !ls_sful_rec->has_lb_vip) {
             continue;
         }
 
@@ -7306,10 +7296,10 @@ build_acl_log(struct ds *actions, const struct nbrec_acl *acl,
 }
 
 static void
-consider_acl(struct hmap *lflows, struct ovn_datapath *od,
+consider_acl(struct hmap *lflows, const struct ovn_datapath *od,
              const struct nbrec_acl *acl, bool has_stateful,
              bool ct_masked_mark, const struct shash *meter_groups,
-             struct ds *match, struct ds *actions)
+             uint64_t max_acl_tier, struct ds *match, struct ds *actions)
 {
     const char *ct_blocked_match = ct_masked_mark
                                    ? "ct_mark.blocked"
@@ -7346,7 +7336,7 @@ consider_acl(struct hmap *lflows, struct ovn_datapath *od,
     /* All ACLS will start by matching on their respective tier. */
     size_t match_tier_len = 0;
     ds_clear(match);
-    if (od->max_acl_tier) {
+    if (max_acl_tier) {
         ds_put_format(match, REG_ACL_TIER " == %"PRId64" && ", acl->tier);
         match_tier_len = match->length;
     }
@@ -7533,12 +7523,15 @@ ovn_update_ipv6_options(struct hmap *lr_ports)
 #define IPV6_CT_OMIT_MATCH "nd || nd_ra || nd_rs || mldv1 || mldv2"
 
 static void
-build_acl_action_lflows(struct ovn_datapath *od, struct hmap *lflows,
+build_acl_action_lflows(const struct ls_stateful_record *ls_stateful_rec,
+                        struct hmap *lflows,
                         const char *default_acl_action,
                         const struct shash *meter_groups,
                         struct ds *match,
                         struct ds *actions)
 {
+    const struct ovn_datapath *od = ls_stateful_rec->od;
+
     enum ovn_stage stages [] = {
         S_SWITCH_IN_ACL_ACTION,
         S_SWITCH_IN_ACL_AFTER_LB_ACTION,
@@ -7549,7 +7542,7 @@ build_acl_action_lflows(struct ovn_datapath *od, struct hmap *lflows,
     ds_put_cstr(actions, REGBIT_ACL_VERDICT_ALLOW " = 0; "
                         REGBIT_ACL_VERDICT_DROP " = 0; "
                         REGBIT_ACL_VERDICT_REJECT " = 0; ");
-    if (od->max_acl_tier) {
+    if (ls_stateful_rec->max_acl_tier) {
         ds_put_cstr(actions, REG_ACL_TIER " = 0; ");
     }
 
@@ -7557,7 +7550,7 @@ build_acl_action_lflows(struct ovn_datapath *od, struct hmap *lflows,
 
     for (size_t i = 0; i < ARRAY_SIZE(stages); i++) {
         enum ovn_stage stage = stages[i];
-        if (!od->has_acls) {
+        if (!ls_stateful_rec->has_acls) {
             ovn_lflow_add(lflows, od, stage, 0, "1", "next;");
             continue;
         }
@@ -7592,7 +7585,7 @@ build_acl_action_lflows(struct ovn_datapath *od, struct hmap *lflows,
         ovn_lflow_add(lflows, od, stage, 0, "1", ds_cstr(actions));
 
         struct ds tier_actions = DS_EMPTY_INITIALIZER;
-        for (size_t j = 0; j < od->max_acl_tier; j++) {
+        for (size_t j = 0; j < ls_stateful_rec->max_acl_tier; j++) {
             ds_clear(match);
             ds_put_format(match, REG_ACL_TIER " == %"PRIuSIZE, j);
             ds_clear(&tier_actions);
@@ -7608,7 +7601,7 @@ build_acl_action_lflows(struct ovn_datapath *od, struct hmap *lflows,
 }
 
 static void
-build_acl_log_related_flows(struct ovn_datapath *od, struct hmap *lflows,
+build_acl_log_related_flows(const struct ovn_datapath *od, struct hmap *lflows,
                             const struct nbrec_acl *acl, bool has_stateful,
                             bool ct_masked_mark,
                             const struct shash *meter_groups,
@@ -7681,15 +7674,19 @@ build_acl_log_related_flows(struct ovn_datapath *od, struct hmap *lflows,
 }
 
 static void
-build_acls(struct ovn_datapath *od, const struct chassis_features *features,
+build_acls(const struct ls_stateful_record *ls_stateful_rec,
+           const struct chassis_features *features,
            struct hmap *lflows,
            const struct ls_port_group_table *ls_port_groups,
            const struct shash *meter_groups)
 {
+    const struct ovn_datapath *od = ls_stateful_rec->od;
+
     const char *default_acl_action = default_acl_drop
                                      ? debug_implicit_drop_action()
                                      : "next;";
-    bool has_stateful = od->has_stateful_acl || od->has_lb_vip;
+    bool has_stateful = (ls_stateful_rec->has_stateful_acl
+                         || ls_stateful_rec->has_lb_vip);
     const char *ct_blocked_match = features->ct_no_masked_label
                                    ? "ct_mark.blocked"
                                    : "ct_label.blocked";
@@ -7703,8 +7700,8 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
      *
      * A related rule at priority 1 is added below if there
      * are any stateful ACLs in this datapath. */
-    if (!od->has_acls) {
-        if (!od->has_lb_vip) {
+    if (!ls_stateful_rec->has_acls) {
+        if (!ls_stateful_rec->has_lb_vip) {
             ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL_EVAL, UINT16_MAX, "1",
                           "next;");
             ovn_lflow_add(lflows, od, S_SWITCH_OUT_ACL_EVAL, UINT16_MAX, "1",
@@ -7867,7 +7864,8 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
                                     meter_groups, &match, &actions);
         consider_acl(lflows, od, acl, has_stateful,
                      features->ct_no_masked_label,
-                     meter_groups, &match, &actions);
+                     meter_groups, ls_stateful_rec->max_acl_tier,
+                     &match, &actions);
     }
 
     const struct ls_port_group *ls_pg =
@@ -7883,7 +7881,8 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
                                             meter_groups, &match, &actions);
                 consider_acl(lflows, od, acl, has_stateful,
                              features->ct_no_masked_label,
-                             meter_groups, &match, &actions);
+                             meter_groups, ls_stateful_rec->max_acl_tier,
+                             &match, &actions);
             }
         }
     }
@@ -7901,7 +7900,7 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
             dns_actions);
     }
 
-    if (od->has_acls || od->has_lb_vip) {
+    if (ls_stateful_rec->has_acls || ls_stateful_rec->has_lb_vip) {
         /* Add a 34000 priority flow to advance the service monitor reply
         * packets to skip applying ingress ACLs. */
         ovn_lflow_add(lflows, od, S_SWITCH_IN_ACL_EVAL, 34000,
@@ -7915,8 +7914,8 @@ build_acls(struct ovn_datapath *od, const struct chassis_features *features,
                     REGBIT_ACL_VERDICT_ALLOW" = 1; next;");
     }
 
-    build_acl_action_lflows(od, lflows, default_acl_action, meter_groups,
-                            &match, &actions);
+    build_acl_action_lflows(ls_stateful_rec, lflows, default_acl_action,
+                            meter_groups, &match, &actions);
 
     ds_destroy(&match);
     ds_destroy(&actions);
@@ -8561,8 +8560,11 @@ build_stateful(struct ovn_datapath *od,
 }
 
 static void
-build_lb_hairpin(struct ovn_datapath *od, struct hmap *lflows)
+build_lb_hairpin(const struct ls_stateful_record *ls_stateful_rec,
+                 struct hmap *lflows)
 {
+    const struct ovn_datapath *od = ls_stateful_rec->od;
+
     /* Ingress Pre-Hairpin/Nat-Hairpin/Hairpin tabled (Priority 0).
      * Packets that don't need hairpinning should continue processing.
      */
@@ -8570,7 +8572,7 @@ build_lb_hairpin(struct ovn_datapath *od, struct hmap *lflows)
     ovn_lflow_add(lflows, od, S_SWITCH_IN_NAT_HAIRPIN, 0, "1", "next;");
     ovn_lflow_add(lflows, od, S_SWITCH_IN_HAIRPIN, 0, "1", "next;");
 
-    if (od->has_lb_vip) {
+    if (ls_stateful_rec->has_lb_vip) {
         /* Check if the packet needs to be hairpinned.
          * Set REGBIT_HAIRPIN in the original direction and
          * REGBIT_HAIRPIN_REPLY in the reply direction.
@@ -9193,9 +9195,7 @@ build_dhcpv4_options_flows(struct ovn_port *op,
                     &op->nbsp->dhcpv4_options->options, "lease_time");
                 ovs_assert(server_id && server_mac && lease_time);
                 const char *dhcp_actions =
-                    (op->od->has_stateful_acl || op->od->has_lb_vip)
-                     ? REGBIT_ACL_VERDICT_ALLOW" = 1; ct_commit; next;"
-                     : REGBIT_ACL_VERDICT_ALLOW" = 1; next;";
+                    REGBIT_ACL_VERDICT_ALLOW" = 1; next;";
                 ds_clear(&match);
                 ds_put_format(&match, "outport == %s && eth.src == %s "
                               "&& ip4.src == %s && udp && udp.src == 67 "
@@ -9278,9 +9278,7 @@ build_dhcpv6_options_flows(struct ovn_port *op,
                 ipv6_string_mapped(server_ip, &lla);
 
                 const char *dhcp6_actions =
-                    (op->od->has_stateful_acl || op->od->has_lb_vip)
-                        ? REGBIT_ACL_VERDICT_ALLOW" = 1; ct_commit; next;"
-                        : REGBIT_ACL_VERDICT_ALLOW" = 1; next;";
+                    REGBIT_ACL_VERDICT_ALLOW" = 1; next;";
                 ds_clear(&match);
                 ds_put_format(&match, "outport == %s && eth.src == %s "
                               "&& ip6.src == %s && udp && udp.src == 547 "
@@ -9387,22 +9385,16 @@ build_lswitch_lflows_l2_unknown(struct ovn_datapath *od,
 static void
 build_lswitch_lflows_pre_acl_and_acl(
     struct ovn_datapath *od,
-    const struct ls_port_group_table *ls_port_groups,
     const struct chassis_features *features,
     struct hmap *lflows,
     const struct shash *meter_groups)
 {
     ovs_assert(od->nbs);
-    ls_get_acl_flags(od, ls_port_groups);
-
-    build_pre_acls(od, ls_port_groups, lflows);
+    build_pre_acls(od, lflows);
     build_pre_lb(od, meter_groups, lflows);
     build_pre_stateful(od, features, lflows);
-    build_acl_hints(od, features, lflows);
-    build_acls(od, features, lflows, ls_port_groups, meter_groups);
     build_qos(od, lflows);
     build_stateful(od, features, lflows);
-    build_lb_hairpin(od, lflows);
     build_vtep_hairpin(od, lflows);
 }
 
@@ -15461,7 +15453,7 @@ build_lrouter_nat_defrag_and_lb(
      * a dynamically negotiated FTP data channel), but will allow
      * related traffic such as an ICMP Port Unreachable through
      * that's generated from a non-listening UDP port.  */
-    if (od->has_lb_vip && features->ct_lb_related) {
+    if (lr_sful_rec->has_lb_vip && features->ct_lb_related) {
         ds_clear(match);
 
         ds_put_cstr(match, "ct.rel && !ct.est && !ct.new");
@@ -15486,7 +15478,7 @@ build_lrouter_nat_defrag_and_lb(
      * Pass the traffic that is already established to the next table with
      * proper flags set.
      */
-    if (od->has_lb_vip) {
+    if (lr_sful_rec->has_lb_vip) {
         ds_clear(match);
 
         ds_put_format(match, "ct.est && !ct.rel && !ct.new && %s.natted",
@@ -15516,7 +15508,7 @@ build_lrouter_nat_defrag_and_lb(
      * not committed, it would produce ongoing datapath flows with the ct.new
      * flag set. Some NICs are unable to offload these flows.
      */
-    if (od->is_gw_router && (od->nbr->n_nat || od->has_lb_vip)) {
+    if (od->is_gw_router && (od->nbr->n_nat || lr_sful_rec->has_lb_vip)) {
         /* Do not send ND or ICMP packets to connection tracking. */
         ovn_lflow_add(lflows, od, S_ROUTER_OUT_UNDNAT, 100,
                       "nd || nd_rs || nd_ra", "next;");
@@ -15941,6 +15933,22 @@ build_lr_stateful_flows(const struct lr_stateful_record *lr_sful_rec,
                                       meter_groups);
 }
 
+static void
+build_ls_stateful_flows(const struct ls_stateful_record *ls_stateful_rec,
+                      const struct ls_port_group_table *ls_pgs,
+                      const struct chassis_features *features,
+                      const struct shash *meter_groups,
+                      struct hmap *lflows)
+{
+    ovs_assert(ls_stateful_rec->od);
+
+    build_ls_stateful_rec_pre_acls(ls_stateful_rec, ls_pgs, lflows);
+    build_ls_stateful_rec_pre_lb(ls_stateful_rec, lflows);
+    build_acl_hints(ls_stateful_rec, features, lflows);
+    build_acls(ls_stateful_rec, features, lflows, ls_pgs, meter_groups);
+    build_lb_hairpin(ls_stateful_rec, lflows);
+}
+
 struct lswitch_flow_build_info {
     const struct ovn_datapaths *ls_datapaths;
     const struct ovn_datapaths *lr_datapaths;
@@ -15948,6 +15956,7 @@ struct lswitch_flow_build_info {
     const struct hmap *lr_ports;
     const struct ls_port_group_table *ls_port_groups;
     const struct lr_stateful_table *lr_sful_table;
+    const struct ls_stateful_table *ls_sful_table;
     struct hmap *lflows;
     struct hmap *igmp_groups;
     const struct shash *meter_groups;
@@ -15972,9 +15981,7 @@ build_lswitch_and_lrouter_iterate_by_ls(struct ovn_datapath *od,
                                         struct lswitch_flow_build_info *lsi)
 {
     ovs_assert(od->nbs);
-    build_lswitch_lflows_pre_acl_and_acl(od, lsi->ls_port_groups,
-                                         lsi->features,
-                                         lsi->lflows,
+    build_lswitch_lflows_pre_acl_and_acl(od, lsi->features, lsi->lflows,
                                          lsi->meter_groups);
 
     build_fwd_group_lflows(od, lsi->lflows);
@@ -16089,6 +16096,7 @@ build_lflows_thread(void *arg)
 {
     struct worker_control *control = (struct worker_control *) arg;
     const struct lr_stateful_record *lr_sful_rec;
+    const struct ls_stateful_record *ls_stateful_rec;
     struct lswitch_flow_build_info *lsi;
     struct ovn_igmp_group *igmp_group;
     struct ovn_lb_datapaths *lb_dps;
@@ -16217,6 +16225,20 @@ build_lflows_thread(void *arg)
                                             lsi->features);
                 }
             }
+
+            for (bnum = control->id;
+                    bnum <= lsi->ls_sful_table->entries.mask;
+                    bnum += control->pool->size)
+            {
+                LS_STATEFUL_TABLE_FOR_EACH_IN_P (ls_stateful_rec, bnum,
+                                                 lsi->ls_sful_table) {
+                    build_ls_stateful_flows(ls_stateful_rec,
+                                            lsi->ls_port_groups,
+                                            lsi->features, lsi->meter_groups,
+                                            lsi->lflows);
+                }
+            }
+
             for (bnum = control->id;
                     bnum <= lsi->igmp_groups->mask;
                     bnum += control->pool->size)
@@ -16277,6 +16299,7 @@ build_lswitch_and_lrouter_flows(const struct ovn_datapaths *ls_datapaths,
                                 const struct hmap *lr_ports,
                                 const struct ls_port_group_table *ls_pgs,
                                 const struct lr_stateful_table *lr_sful_table,
+                                const struct ls_stateful_table *ls_sful_table,
                                 struct hmap *lflows,
                                 struct hmap *igmp_groups,
                                 const struct shash *meter_groups,
@@ -16307,6 +16330,7 @@ build_lswitch_and_lrouter_flows(const struct ovn_datapaths *ls_datapaths,
             lsiv[index].lr_ports = lr_ports;
             lsiv[index].ls_port_groups = ls_pgs;
             lsiv[index].lr_sful_table = lr_sful_table;
+            lsiv[index].ls_sful_table = ls_sful_table;
             lsiv[index].igmp_groups = igmp_groups;
             lsiv[index].meter_groups = meter_groups;
             lsiv[index].lb_dps_map = lb_dps_map;
@@ -16332,6 +16356,7 @@ build_lswitch_and_lrouter_flows(const struct ovn_datapaths *ls_datapaths,
         free(lsiv);
     } else {
         const struct lr_stateful_record *lr_sful_rec;
+        const struct ls_stateful_record *ls_stateful_rec;
         struct ovn_igmp_group *igmp_group;
         struct ovn_lb_datapaths *lb_dps;
         struct ovn_datapath *od;
@@ -16344,6 +16369,7 @@ build_lswitch_and_lrouter_flows(const struct ovn_datapaths *ls_datapaths,
             .lr_ports = lr_ports,
             .ls_port_groups = ls_pgs,
             .lr_sful_table = lr_sful_table,
+            .ls_sful_table = ls_sful_table,
             .lflows = lflows,
             .igmp_groups = igmp_groups,
             .meter_groups = meter_groups,
@@ -16413,6 +16439,12 @@ build_lswitch_and_lrouter_flows(const struct ovn_datapaths *ls_datapaths,
                                     lsi.features);
         }
 
+        LS_STATEFUL_TABLE_FOR_EACH (ls_stateful_rec, ls_sful_table) {
+            build_ls_stateful_flows(ls_stateful_rec, lsi.ls_port_groups,
+                                  lsi.features, lsi.meter_groups,
+                                  lsi.lflows);
+        }
+
         stopwatch_start(LFLOWS_IGMP_STOPWATCH_NAME, time_msec());
         HMAP_FOR_EACH (igmp_group, hmap_node, igmp_groups) {
             build_lswitch_ip_mcast_igmp_mld(igmp_group,
@@ -16509,6 +16541,7 @@ void build_lflows(struct ovsdb_idl_txn *ovnsb_txn,
                                     input_data->lr_ports,
                                     input_data->ls_port_groups,
                                     input_data->lr_sful_table,
+                                    input_data->ls_sful_table,
                                     lflows,
                                     &igmp_groups,
                                     input_data->meter_groups,
diff --git a/northd/northd.h b/northd/northd.h
index 612b292da1..7727b5a8f6 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -89,6 +89,8 @@ ods_size(const struct ovn_datapaths *datapaths)
     return hmap_count(&datapaths->datapaths);
 }
 
+bool od_has_lb_vip(const struct ovn_datapath *od);
+
 struct tracked_ovn_ports {
     /* tracked created ports.
      * hmapx node data is 'struct ovn_port *' */
@@ -118,6 +120,8 @@ enum northd_tracked_data_type {
     NORTHD_TRACKED_PORTS    = (1 << 0),
     NORTHD_TRACKED_LBS      = (1 << 1),
     NORTHD_TRACKED_LR_NATS  = (1 << 2),
+    NORTHD_TRACKED_LS_LBS   = (1 << 3),
+    NORTHD_TRACKED_LS_ACLS  = (1 << 4),
 };
 
 /* Track what's changed in the northd engine node.
@@ -132,6 +136,14 @@ struct northd_tracked_data {
     /* Tracked logical routers whose NATs have changed.
      * hmapx node is 'struct ovn_datapath *'. */
     struct hmapx lr_with_changed_nats;
+
+    /* Tracked logical switches whose load balancers have changed.
+     * hmapx node is 'struct ovn_datapath *'. */
+    struct hmapx ls_with_changed_lbs;
+
+    /* Tracked logical switches whose ACLs have changed.
+     * hmapx node is 'struct ovn_datapath *'. */
+    struct hmapx ls_with_changed_acls;
 };
 
 struct northd_data {
@@ -180,6 +192,7 @@ struct lflow_input {
     const struct hmap *lr_ports;
     const struct ls_port_group_table *ls_port_groups;
     const struct lr_stateful_table *lr_sful_table;
+    const struct ls_stateful_table *ls_sful_table;
     const struct shash *meter_groups;
     const struct hmap *lb_datapaths_map;
     const struct hmap *bfd_connections;
@@ -289,11 +302,7 @@ struct ovn_datapath {
     struct hmap port_tnlids;
     uint32_t port_key_hint;
 
-    bool has_stateful_acl;
-    bool has_lb_vip;
     bool has_unknown;
-    bool has_acls;
-    uint64_t max_acl_tier;
     bool has_vtep_lports;
     bool has_arp_proxy_port;
 
@@ -538,6 +547,18 @@ northd_has_lr_nats_in_tracked_data(struct northd_tracked_data *trk_nd_changes)
     return (trk_nd_changes->type & NORTHD_TRACKED_LR_NATS);
 }
 
+static inline bool
+northd_has_ls_lbs_in_tracked_data(struct northd_tracked_data *trk_nd_changes)
+{
+    return (trk_nd_changes->type & NORTHD_TRACKED_LS_LBS);
+}
+
+static inline bool
+northd_has_ls_acls_in_tracked_data(struct northd_tracked_data *trk_nd_changes)
+{
+    return (trk_nd_changes->type & NORTHD_TRACKED_LS_ACLS);
+}
+
 /* Returns 'true' if the IPv4 'addr' is on the same subnet with one of the
  * IPs configured on the router port.
  */
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index e85e00cc9b..084d675644 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -872,6 +872,7 @@ main(int argc, char *argv[])
     stopwatch_create(SYNC_METERS_RUN_STOPWATCH_NAME, SW_MS);
     stopwatch_create(LR_NAT_RUN_STOPWATCH_NAME, SW_MS);
     stopwatch_create(LR_STATEFUL_RUN_STOPWATCH_NAME, SW_MS);
+    stopwatch_create(LS_STATEFUL_RUN_STOPWATCH_NAME, SW_MS);
 
     /* Initialize incremental processing engine for ovn-northd */
     inc_proc_northd_init(&ovnnb_idl_loop, &ovnsb_idl_loop);
