Repository: kiali/kiali

Issue:
Kiali uses a lot of CPU (in validations)
### Describe the bug

On a cluster with around 1600 namespaces, but a discovery selector that only selects around 380 of them and cluster wide view set to true, the CPU usage of the Kiali pod is permanently around 2500m. Although I have no reference to compare it with, this seems a bit excessive. This is even with any clients interacting with the UI.

![image](https://github.com/user-attachments/assets/7acaf779-3d2e-4291-8fa0-9586d63a27ab)

The graph shows a drop in usage. With cluster wide view disabled, CPU load is around 4000m.

### Expected Behavior

I'd like to understand if this a reasonable amount of CPU for our setup or if there is an issue with Kiali that causes it to use so much CPU.

### What are the steps to reproduce this bug?

Nothing in particular.
Cluster has around 1600 namespaces in total, but only 380 are selected with a discovery selector. Cluster wide view is enabled. Anonymous mode enabled.

### Environment

- **Kiali version:** 2.2.0
- **Istio version:** 1.23.2 asm 2
- **Kubernetes impl:** GKE
- **Kubernetes version:** 1.30
- **Other notable environmental factors:**

Issues/PRs for this Epic:
- [x] https://github.com/kiali/kiali/pull/8119
- [x] https://github.com/kiali/kiali/pull/8194
- [x] https://github.com/kiali/kiali/pull/8206
- [x] #8211 
- [x] #8210 
- [x] [pre-compute/faster validation keys for istio config objects](https://github.com/kiali/kiali/pull/8335)

Discussion:
@rolandkool: [profile.gz](https://github.com/user-attachments/files/18168277/profile.gz)

I've attached a 15 seconds CPU profile.@rolandkool: Deployed another instance on a cluster with similar amount of total namespaces, but only 25 are included in the discovery selector. CPU usage is permanently around 600m.@jshaughn: I think currently the Kiali Discovery Selector is gating the namespaces available to, or can be seen, by the user.  But I don't think it has the ability to reduce the number of k8s watches that are in place in cluster-wide, to manage caching. And so I think that is why you see more of a constant consumption based on the total namespaces.  

I think the workaround is to not use cluster-wide, despite the fact that it is often the more efficient option.  I think we will need to investigate whether we can apply Discovery Selectors to also minimize cache impact.

cc @jmazzitelli , @nrfox @rolandkool: @jshaughn thanks for your answer. I tried without cluster-wide access, but that makes the CPU usage even higher (see graph in description above. 4 cpu is without cluster wide access, 2.5 is with).
@jshaughn: OK @rolandkool , thanks for the update. I will add this to the backlog to see what we may be able to do.@frittentheke: @jshaughn  I am no expert in hunting down performance issues using pprof... but by peeking into the profile @rolandkool  provided, it seems that all of that CPU time is spent somewhere in https://github.com/kiali/kiali/blob/master/business/istio_validations.go:

![Image](https://github.com/user-attachments/assets/d5810e86-1bb9-4501-8107-83fdcfc95b05)

@rolandkool  do you happen to have more details on e.g. the reconcile time. Should be logged at `debug` level, see https://github.com/kiali/kiali/blob/cf8785962ffd76bfa861df35a3de8d102fdd2e5f/controller/validations.go#L111)@hhovsepy: This is the Istio configs validations cache reconciliation, which starts in a background every 10 seconds, does the validation of all Istio config objects and sets it into the cache to be reused by Kiali. The caching mechanism was applied for the Kiali pages loading time optimization.@jshaughn: Thanks, @frittentheke .  We will have to take a closer look at this, and see what we can do.@frittentheke: There also are all those `/metrics` exported by Kiali ... if @rolandkool was collecting them, that might also narrow down where Kiali was spending most of its time.@jshaughn: cc @nrfox 

@hhovsepy , I understand why we want to cache validation results; doing them on-demand creates UI lag for the user, and adding the new controller was a great UX perf enhancement. But I am questioning the frequency of recalculation.  In general, I don't think configuration frequently changes, and so every 10s seems quite aggressive. Of course it's very safe, and prevents obsolete validations, but it's having a noticeable effect on the Kiali pod (and possibly the cost of running Kiali).

I know we have talked about a watcher-based approach, that watches for config changes and reconciles validations only in response to a change.  Is this a feasible approach at this time?  Otherwise, I think we need to cache validations at Kiali startup and then only after a configurable period.  And the default period should likely be much greater than 10s.  Also, it doesn't have to be "all or nothing", we could use a timer and also add watchers incrementally.

@hhovsepy: Note for future work: We need to make sure that Istio configurations edited outside of the Kiali are also included into validations by a rerun or a watcher-based.@rolandkool: @frittentheke 
From the debug logging, I see this:

> 2025-02-04T15:17:54Z DBG [ValidationsReconciler] Started reconciling 
2025-02-04T15:19:34Z DBG [ValidationsReconciler] Finished reconciling in 100469ms
2025-02-04T15:19:34Z WRN [ValidationsReconciler] Reconcile took longer than the reconcile interval of [10s]. If this continues, validations will be increasingly stale. Please ensure your environment is healthy. If the issue still persists, please open an issue at www.github.com/kiali/kiali
2025-02-04T15:19:34Z DBG [ValidationsReconciler] Started reconciling 
2025-02-04T15:21:15Z DBG [ValidationsReconciler] Finished reconciling in 100436ms
2025-02-04T15:21:15Z WRN [ValidationsReconciler] Reconcile took longer than the reconcile interval of [10s]. If this continues, validations will be increasingly stale. Please ensure your environment is healthy. If the issue still persists, please open an issue at www.github.com/kiali/kiali
2025-02-04T15:21:15Z DBG [ValidationsReconciler] Started reconciling 

So it seems indeed that it is in a constant reconcile loop.
It takes about a 100 seconds to do a single reconcile (on the larger cluster), and when it has only 10s to do so, it starts a new one right away. On the smaller cluster, a reconcile takes 2 seconds. The load is around 500m on that cluster.

Feels like this 10s is a bit too aggressive. The configuration is not that volatile. It would be nice if it would only reconcile on an event or otherwise just allow a different reconcile interval. For example, 1 hour is probably sufficient for us. Most of our users are interested in the graph and dependencies between services.

Some possible interesting metrics (from a Kiali instance that has been running for about 7 minutes):
```
kiali_checker_processing_duration_seconds_sum{checker="checkers.AuthorizationPolicyChecker"} 4.570136205000005
kiali_checker_processing_duration_seconds_sum{checker="checkers.DestinationRulesChecker"} 1.3105037879999986
kiali_checker_processing_duration_seconds_sum{checker="checkers.GatewayChecker"} 0.0010832109999999995
kiali_checker_processing_duration_seconds_sum{checker="checkers.K8sGRPCRouteChecker"} 0.001104912
kiali_checker_processing_duration_seconds_sum{checker="checkers.K8sGatewayChecker"} 0.001285068999999999
kiali_checker_processing_duration_seconds_sum{checker="checkers.K8sHTTPRouteChecker"} 0.0007545829999999994
kiali_checker_processing_duration_seconds_sum{checker="checkers.K8sReferenceGrantChecker"} 0.000568709
kiali_checker_processing_duration_seconds_sum{checker="checkers.NoServiceChecker"} 43.686485464999976
kiali_checker_processing_duration_seconds_sum{checker="checkers.PeerAuthenticationChecker"} 0.0027431110000000047
kiali_checker_processing_duration_seconds_sum{checker="checkers.RequestAuthenticationChecker"} 33.374142512
kiali_checker_processing_duration_seconds_sum{checker="checkers.ServiceEntryChecker"} 2.1402931099999973
kiali_checker_processing_duration_seconds_sum{checker="checkers.SidecarChecker"} 66.75497811800005
kiali_checker_processing_duration_seconds_sum{checker="checkers.TelemetryChecker"} 0.0002582520000000002
kiali_checker_processing_duration_seconds_sum{checker="checkers.VirtualServiceChecker"} 31.964724545999957
kiali_checker_processing_duration_seconds_sum{checker="checkers.WasmPluginChecker"} 0.0002953839999999999
kiali_checker_processing_duration_seconds_sum{checker="checkers.WorkloadChecker"} 4.909717194000001
kiali_validation_processing_duration_seconds_sum{namespace="_all_",service="_all_"} 293.271228488
```
@jshaughn: Thanks @rolandkool.  Right, the fact that the reconcile is longer than the interval is not good.  Also, I'm a little curious about what we're doing that takes so long to calculate, we may want to do some detailed profiling of that validation logic.  I think event-based reconcile would be best, but otherwise we need a configurable intervale, with a much longer default.  Also, the next reconcile could depend on the length of the previous reconcile duration.@nrfox: > Some possible interesting metrics (from a Kiali instance that has been running for about 7 minutes):

This is great data, thank you!

> Feels like this 10s is a bit too aggressive. The configuration is not that volatile. It would be nice if it would only reconcile on an event or otherwise just allow a different reconcile interval. For example, 1 hour is probably sufficient for us. Most of our users are interested in the graph and dependencies between services.

Yes we should provide a config option to tune and/or disable this. The reason it was set so low to begin with was to ensure that when you make a config change, you get relatively fast feedback on whether or not that config change was valid. But there's also other tools you can use to validate your config, like `istioctl analyze`, so it's perfectly reasonable to set this higher and/or disable it altogether.

Moving to event based reconciling would help overall reconcile times after the initial validation. There's an underlying perf issue with validations that still needs to be addressed though. Moving validations to background processing was a way of improving the user experience but it did not deal with the underlying issue. Before we moved validations to being processed in the background, the validations requests would sometimes takes 1-2 minutes which would lead to significant slowdowns in the UI. It's now a better experience but obviously there's now the issue of Kiali constantly consuming resources if validations are slow. @hhovsepy: To summarize the solution directions, we can create the following sub-issues to resolve and implement:

- [ ]  Make the reconciliation interval configurable and/or provide an option to disable it.
- [ ] Implement event-based reconciliation triggered by configuration changes outside of Kiali.
- [ ] Optimize the validation process to reduce resource usage.@nrfox: Yeah and in terms of level of effort:

This one is very trivial
> Make the reconciliation interval configurable and/or provide an option to disable it.

This one is a medium-large effort
> Implement event-based reconciliation triggered by configuration changes outside of Kiali.

This one requires some investigation
> Optimize the validation process to reduce resource usage.

IMO we should prioritize 1 and 3.@jshaughn: I agree with the prioritization of 1 and 3.  Let's get the setting configurable asap and change the default to what, 60m?  Also, don't start the timer until after a reconcile completes.

Do we reconcile when Kiali itself is changing/updating config?

After that change is in, let's prioritize the 3rd task, try to understand if we can speed up the reconcile.@hhovsepy: 
> 
> Do we reconcile when Kiali itself is changing/updating config?
> 
Yeas each time Kiali edits a config, it is reconciled.

@nrfox: > Let's get the setting configurable asap and change the default to what, 60m?

60m is very large. Validations will be noticeably stale at that point. `60s`? We'll keep the warning so that if you start to go past that interval then you'll at least get a warning message telling you to adjust the config option.@rolandkool: Thanks, looking forward for the fix. That will allow me to roll out Kiali across more clusters. This high usage was holding me back.
I can help testing a pre release if you like.

@frittentheke: @nrfox @jshaughn could you possibly compile a few instructions on how to do e.g. pprof profiling that actually would help you narrow down this issue? Certainly it makes sense to also (!) tackle the underlying (potential) performance issues.
Years ago I also was involved in https://github.com/kiali/kiali/issues/4592 which was (in the end) quite easily found when looking at the profiling in the real environment.@nrfox: @frittentheke you can have a look at https://kiali.io/docs/configuration/debugging-kiali/#profiler for how to enable the profiler.@frittentheke: > [@frittentheke](https://github.com/frittentheke) you can have a look at https://kiali.io/docs/configuration/debugging-kiali/#profiler for how to enable the profiler.

@rolandkool As I don't see similar durations as you do, could you potentially run some more pprof ?@rolandkool: [profile.gz](https://github.com/user-attachments/files/18672578/profile.gz)

Here's another 15 seconds profile.

The profile command on the /debug/pprof page does not work, you need to manually add `&seconds=<nr>` to the URL.
@nrfox: @rolandkool as of v2.6 you can set `external_services.istio.validation_reconcile_interval` to have more control over how often Kiali will perform validations: https://kiali.io/docs/configuration/kialis.kiali.io/#.spec.external_services.istio.validation_reconcile_interval. You can see if that helps with the CPU usage.@jshaughn: I'm going to take a look, I'm unfamiliar with the validation logic, so it may take some time...@rolandkool: @nrfox thanks for implementing this setting. That definitely helps in our setup. I've set the reconcile interval to 15m, which is good enough for us for now. When not reconciling, the load drops to a more manageable 50-100m (depending on whether the UI is used or not).

Looking forward to a rework of this logic so that it only processes delta's instead of reconciling the whole config each time.
@frittentheke: @jshaughn we don't observe the issue in the scale as reported here, but if you need any more data (pprof) let me know. I suppose @rolandkool is just as willing to provide any debug data you require to figure out the suspected inefficiency here.@jshaughn: @frittentheke , thanks, I may accept the offer. At the moment, though, I am just familiarizing myself with the code and doing some initial re-arranging.  I'll let you know!@nrfox: If you look at the cpu profile a lot of time seems to be spent on garbage collection:

![Image](https://github.com/user-attachments/assets/897324ab-3438-46e1-ba34-d6acb0287b9e)

Reducing the number of allocations we do in validations should help with this.@nrfox: > Reducing the number of allocations we do in validations should help with this.

That being said, if this is going to take a large scale refactor, it would be nice to at least investigate this issue first: https://github.com/kiali/kiali/issues/4713 since the results of that may also require a refactor.@jshaughn: > Reducing the number of allocations we do in validations should help with this.

Is that using the old code or the new code?@frittentheke: > > Reducing the number of allocations we do in validations should help with this.
> 
> That being said, if this is going to take a large scale refactor, it would be nice to at least investigate this issue first: https://github.com/kiali/kiali/issues/4713 since the results of that may also require a refactor.

+1

I was always wondering why Kiali did his own tests and not built on top of istio analyse (even contribute missing consistency and housekeeping checks there).@nrfox: > Is that using the old code or the new code?

This was looking at the profile linked in this issue but I've seen similar results when running a local benchmark: https://github.com/kiali/kiali/pull/8208@nrfox: > I was always wondering why Kiali did his own tests and not built on top of istio analyse (even contribute missing consistency and housekeeping checks there).

iirc Kiali validations predate istio analyze but istio analyze has been around for awhile now@frittentheke: > > I was always wondering why Kiali did his own tests and not built on top of istio analyse (even contribute missing consistency and housekeeping checks there).
> 
> iirc Kiali validations predate istio analyze but istio analyze has been around for awhile now

thanks for https://github.com/kiali/kiali/issues/4713#issuecomment-2698846877 @nrfox @jshaughn: > If you look at the cpu profile a lot of time seems to be spent on garbage collection:

@nrfox The work for #8223 should help a lot with this.  See the pprof results that @hhovsepy has produced.@jshaughn: @frittentheke , @rolandkool , Kiali 2.7 will contain several changes that we hope will provide perf benefits, particularly for
validation.  It is scheduled for release Monday, Mar 17 (Happy St Patrick's Day!)  When/if you get the chance, please let us know if you are seeing improvement.@rolandkool: @jshaughn I've just deployed 2.7.1 on our clusters. With a reconcile interval of 15 minutes, I do see that each reconcile takes less time (up to a minute less). Still heavy on CPU though. @jshaughn: Thanks for the feedback, @rolandkool. I would have expected the first reconcile to be a bit faster, but assuming no config changes, I would have expected subsequent reconciles to be improved a fair amount.  This is because we added some logic to detect configuration changes.  We'll keep looking for ways to improve.@rolandkool: Here's a comparison.
On 2.6:

![Image](https://github.com/user-attachments/assets/b7ca4303-eafb-4db9-a94f-27b5e59b4dd3)

On 2.7.1:

![Image](https://github.com/user-attachments/assets/92a37545-68db-48bb-ab37-2266f1bd47aa)
@jshaughn: Thanks, @rolandkool .  What this shows me is that the CPU needed is still a spike, although the spike is generally for less time and the avg is down.  I expect that memory consumption is also reduced quite a bit during these periods.  Assuming the change detection code is working as expected (there would be trace level logs saying `"validations: no changes for cluster XXX, re-using"`), the change detection still consumes about 1/2 - 2/3 or the resources.  The savings are decent, although validation is still resource intensive; that's likely the nature of validation and all of the cross-checking that it performs.@jmazzitelli: Should Kial provide a on/off switch for validation entirely? For those environments that simply can't support it without killing resources, just have people turn it off entirely.@jshaughn: > Should Kial provide a on/off switch for validation entirely? 

I don't think so. Validations are a valuable tool and the reconcile interval is configurable.  But, if requested we could do something like have a reconcile interval <= 0 be a disable.@jshaughn: This round of validation perf enhancements is complete. It can still be intensive but we've made significant reductions to CPU and memory. We will also allow disabling validation in https://github.com/kiali/kiali/issues/8317.

Solution patch:
diff --git a/business/checkers/authorization_policies_checker.go b/business/checkers/authorization_policies_checker.go
index 546719a7e..99899867c 100644
--- a/business/checkers/authorization_policies_checker.go
+++ b/business/checkers/authorization_policies_checker.go
@@ -20,7 +20,7 @@ type AuthorizationPolicyChecker struct {
 	ServiceEntries        []*networking_v1.ServiceEntry
 	AuthorizationPolicies []*security_v1.AuthorizationPolicy
 	VirtualServices       []*networking_v1.VirtualService
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 }
 
 func (a AuthorizationPolicyChecker) Check() models.IstioValidations {
diff --git a/business/checkers/common/multi_match_selector_checker.go b/business/checkers/common/multi_match_selector_checker.go
index 59429ee3a..597b2a84a 100644
--- a/business/checkers/common/multi_match_selector_checker.go
+++ b/business/checkers/common/multi_match_selector_checker.go
@@ -14,12 +14,12 @@ type GenericMultiMatchChecker struct {
 	ObjectGVK             schema.GroupVersionKind
 	Keys                  []models.IstioValidationKey
 	Selectors             map[int]map[string]string
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	Path                  string
 	skipSelSubj           bool
 }
 
-func PeerAuthenticationMultiMatchChecker(cluster string, objectGVK schema.GroupVersionKind, pa []*security_v1.PeerAuthentication, workloadsPerNamespace map[string]models.WorkloadList) GenericMultiMatchChecker {
+func PeerAuthenticationMultiMatchChecker(cluster string, objectGVK schema.GroupVersionKind, pa []*security_v1.PeerAuthentication, workloadsPerNamespace map[string]models.Workloads) GenericMultiMatchChecker {
 	keys := []models.IstioValidationKey{}
 	selectors := make(map[int]map[string]string, len(pa))
 	for i, p := range pa {
@@ -47,7 +47,7 @@ func PeerAuthenticationMultiMatchChecker(cluster string, objectGVK schema.GroupV
 	}
 }
 
-func RequestAuthenticationMultiMatchChecker(cluster string, objectGVK schema.GroupVersionKind, ra []*security_v1.RequestAuthentication, workloadsPerNamespace map[string]models.WorkloadList) GenericMultiMatchChecker {
+func RequestAuthenticationMultiMatchChecker(cluster string, objectGVK schema.GroupVersionKind, ra []*security_v1.RequestAuthentication, workloadsPerNamespace map[string]models.Workloads) GenericMultiMatchChecker {
 	keys := []models.IstioValidationKey{}
 	selectors := make(map[int]map[string]string, len(ra))
 	for i, r := range ra {
@@ -76,13 +76,13 @@ func RequestAuthenticationMultiMatchChecker(cluster string, objectGVK schema.Gro
 	}
 }
 
-func SidecarSelectorMultiMatchChecker(cluster string, objectGVK schema.GroupVersionKind, sc []*networking_v1.Sidecar, workloadsPerNamespace map[string]models.WorkloadList) GenericMultiMatchChecker {
+func SidecarSelectorMultiMatchChecker(cluster string, objectGVK schema.GroupVersionKind, sc []*networking_v1.Sidecar, workloadsPerNamespace map[string]models.Workloads) GenericMultiMatchChecker {
 	keys := []models.IstioValidationKey{}
 	selectors := make(map[int]map[string]string, len(sc))
 	i := 0
 	for _, s := range sc {
-		for _, wls := range workloadsPerNamespace {
-			if s.Namespace != wls.Namespace {
+		for ns, _ := range workloadsPerNamespace {
+			if s.Namespace != ns {
 				// Workloads from Sidecar's own Namespaces only are considered in Selector
 				continue
 			}
@@ -227,12 +227,12 @@ func (m GenericMultiMatchChecker) multiMatchSubjects() ReferenceMap {
 			continue
 		}
 
-		for _, wls := range m.WorkloadsPerNamespace {
-			for _, w := range wls.Workloads {
+		for ns, workloads := range m.WorkloadsPerNamespace {
+			for _, w := range workloads {
 				if !selector.Matches(labels.Set(w.Labels)) {
 					continue
 				}
-				workloadKey := models.BuildKey(w.WorkloadGVK, w.Name, wls.Namespace, m.Cluster)
+				workloadKey := models.BuildKey(w.WorkloadGVK, w.Name, ns, m.Cluster)
 				workloadSubjects.Add(workloadKey, subjectKey)
 			}
 		}
diff --git a/business/checkers/common/workload_selector_checker.go b/business/checkers/common/workload_selector_checker.go
index 3d999787c..aeced6c3d 100644
--- a/business/checkers/common/workload_selector_checker.go
+++ b/business/checkers/common/workload_selector_checker.go
@@ -10,11 +10,11 @@ import (
 type GenericNoWorkloadFoundChecker struct {
 	ObjectGVK             schema.GroupVersionKind
 	SelectorLabels        map[string]string
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	Path                  string
 }
 
-func SelectorNoWorkloadFoundChecker(objectGVK schema.GroupVersionKind, selectorLabels map[string]string, workloadsPerNamespace map[string]models.WorkloadList) GenericNoWorkloadFoundChecker {
+func SelectorNoWorkloadFoundChecker(objectGVK schema.GroupVersionKind, selectorLabels map[string]string, workloadsPerNamespace map[string]models.Workloads) GenericNoWorkloadFoundChecker {
 	return GenericNoWorkloadFoundChecker{
 		ObjectGVK:             objectGVK,
 		SelectorLabels:        selectorLabels,
@@ -23,7 +23,7 @@ func SelectorNoWorkloadFoundChecker(objectGVK schema.GroupVersionKind, selectorL
 	}
 }
 
-func WorkloadSelectorNoWorkloadFoundChecker(objectGVK schema.GroupVersionKind, selectorLabels map[string]string, workloadsPerNamespace map[string]models.WorkloadList) GenericNoWorkloadFoundChecker {
+func WorkloadSelectorNoWorkloadFoundChecker(objectGVK schema.GroupVersionKind, selectorLabels map[string]string, workloadsPerNamespace map[string]models.Workloads) GenericNoWorkloadFoundChecker {
 	return GenericNoWorkloadFoundChecker{
 		ObjectGVK:             objectGVK,
 		SelectorLabels:        selectorLabels,
@@ -47,8 +47,8 @@ func (wsc GenericNoWorkloadFoundChecker) Check() ([]*models.IstioCheck, bool) {
 func (wsc GenericNoWorkloadFoundChecker) hasMatchingWorkload(labelSelector map[string]string) bool {
 	selector := labels.SelectorFromSet(labelSelector)
 
-	for _, wls := range wsc.WorkloadsPerNamespace {
-		for _, wl := range wls.Workloads {
+	for _, workloads := range wsc.WorkloadsPerNamespace {
+		for _, wl := range workloads {
 			wlLabelSet := labels.Set(wl.Labels)
 			if selector.Matches(wlLabelSet) {
 				return true
diff --git a/business/checkers/destinationrules/no_dest_checker.go b/business/checkers/destinationrules/no_dest_checker.go
index b6ba25017..944b0ec44 100644
--- a/business/checkers/destinationrules/no_dest_checker.go
+++ b/business/checkers/destinationrules/no_dest_checker.go
@@ -12,7 +12,7 @@ import (
 
 type NoDestinationChecker struct {
 	Namespaces            models.Namespaces
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	DestinationRule       *networking_v1.DestinationRule
 	VirtualServices       []*networking_v1.VirtualService
 	ServiceEntries        []*networking_v1.ServiceEntry
@@ -97,10 +97,10 @@ func (n NoDestinationChecker) hasMatchingWorkload(host kubernetes.Host, subsetLa
 	if len(selectors) != 0 {
 		selector := labels.SelectorFromSet(labels.Set(selectors))
 
-		for _, wl := range n.WorkloadsPerNamespace[localNs].Workloads {
-			wlLabelSet := labels.Set(wl.Labels)
-			if selector.Matches(wlLabelSet) {
-				if subsetSelector.Matches(wlLabelSet) {
+		for _, w := range n.WorkloadsPerNamespace[localNs] {
+			wLabelSet := labels.Set(w.Labels)
+			if selector.Matches(wLabelSet) {
+				if subsetSelector.Matches(wLabelSet) {
 					return true
 				}
 			}
@@ -130,7 +130,7 @@ func (n NoDestinationChecker) hasMatchingService(host kubernetes.Host, itemNames
 
 	if localNs == itemNamespace {
 		// Check Workloads
-		if matches := kubernetes.HasMatchingWorkloads(localSvc, n.WorkloadsPerNamespace[localNs].GetLabels()); matches {
+		if matches := kubernetes.HasMatchingWorkloads(localSvc, models.GetLabels(n.WorkloadsPerNamespace[localNs])); matches {
 			return matches
 		}
 	}
diff --git a/business/checkers/gateway_checker.go b/business/checkers/gateway_checker.go
index 6366d8dbd..9da70aacb 100644
--- a/business/checkers/gateway_checker.go
+++ b/business/checkers/gateway_checker.go
@@ -10,7 +10,7 @@ import (
 
 type GatewayChecker struct {
 	Gateways              []*networking_v1.Gateway
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	IsGatewayToNamespace  bool
 	Cluster               string
 }
diff --git a/business/checkers/gateways/selector_checker.go b/business/checkers/gateways/selector_checker.go
index 97eaf4efd..692a67819 100644
--- a/business/checkers/gateways/selector_checker.go
+++ b/business/checkers/gateways/selector_checker.go
@@ -8,7 +8,7 @@ import (
 )
 
 type SelectorChecker struct {
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	Gateway               *networking_v1.Gateway
 	IsGatewayToNamespace  bool
 }
@@ -26,12 +26,12 @@ func (s SelectorChecker) Check() ([]*models.IstioCheck, bool) {
 func (s SelectorChecker) hasMatchingWorkload(labelSelector map[string]string) bool {
 	selector := labels.SelectorFromSet(labelSelector)
 
-	for _, wls := range s.WorkloadsPerNamespace {
-		if s.IsGatewayToNamespace && wls.Namespace != s.Gateway.Namespace {
+	for ns, workloads := range s.WorkloadsPerNamespace {
+		if s.IsGatewayToNamespace && ns != s.Gateway.Namespace {
 			continue
 		}
-		for _, wl := range wls.Workloads {
-			wlLabelSet := labels.Set(wl.Labels)
+		for _, w := range workloads {
+			wlLabelSet := labels.Set(w.Labels)
 			if selector.Matches(wlLabelSet) {
 				return true
 			}
diff --git a/business/checkers/no_service_checker.go b/business/checkers/no_service_checker.go
index 5d3d6a41e..d1a6e4eab 100644
--- a/business/checkers/no_service_checker.go
+++ b/business/checkers/no_service_checker.go
@@ -12,7 +12,7 @@ import (
 type NoServiceChecker struct {
 	Namespaces            models.Namespaces
 	IstioConfigList       *models.IstioConfigList
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	AuthorizationDetails  *kubernetes.RBACDetails
 	RegistryServices      []*kubernetes.RegistryService
 	PolicyAllowAny        bool
@@ -71,7 +71,7 @@ func runGatewayCheck(virtualService *networking_v1.VirtualService, gatewayNames
 	return models.IstioValidations{key: validations}
 }
 
-func runDestinationRuleCheck(destinationRule *networking_v1.DestinationRule, workloads map[string]models.WorkloadList,
+func runDestinationRuleCheck(destinationRule *networking_v1.DestinationRule, workloads map[string]models.Workloads,
 	serviceEntries []*networking_v1.ServiceEntry, clusterNamespaces models.Namespaces, registryStatus []*kubernetes.RegistryService, virtualServices []*networking_v1.VirtualService,
 	policyAllowAny bool, cluster string) models.IstioValidations {
 	key, validations := EmptyValidValidation(destinationRule.Name, destinationRule.Namespace, kubernetes.DestinationRules, cluster)
diff --git a/business/checkers/peer_authentication_checker.go b/business/checkers/peer_authentication_checker.go
index d4296e052..94c2e85c7 100644
--- a/business/checkers/peer_authentication_checker.go
+++ b/business/checkers/peer_authentication_checker.go
@@ -13,7 +13,7 @@ import (
 type PeerAuthenticationChecker struct {
 	PeerAuthentications   []*security_v1.PeerAuthentication
 	MTLSDetails           kubernetes.MTLSDetails
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	Cluster               string
 }
 
diff --git a/business/checkers/request_authentication_checker.go b/business/checkers/request_authentication_checker.go
index cabf526fb..a530c6f74 100644
--- a/business/checkers/request_authentication_checker.go
+++ b/business/checkers/request_authentication_checker.go
@@ -10,7 +10,7 @@ import (
 
 type RequestAuthenticationChecker struct {
 	RequestAuthentications []*security_v1.RequestAuthentication
-	WorkloadsPerNamespace  map[string]models.WorkloadList
+	WorkloadsPerNamespace  map[string]models.Workloads
 	Cluster                string
 }
 
diff --git a/business/checkers/sidecars_checker.go b/business/checkers/sidecars_checker.go
index 912afb756..af44b66a1 100644
--- a/business/checkers/sidecars_checker.go
+++ b/business/checkers/sidecars_checker.go
@@ -13,7 +13,7 @@ type SidecarChecker struct {
 	Sidecars              []*networking_v1.Sidecar
 	ServiceEntries        []*networking_v1.ServiceEntry
 	Namespaces            models.Namespaces
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	RegistryServices      []*kubernetes.RegistryService
 	Cluster               string
 }
diff --git a/business/checkers/workloads/uncovered_workload_checker.go b/business/checkers/workloads/uncovered_workload_checker.go
index 67ebfa01c..4d68c7027 100644
--- a/business/checkers/workloads/uncovered_workload_checker.go
+++ b/business/checkers/workloads/uncovered_workload_checker.go
@@ -9,7 +9,7 @@ import (
 )
 
 type UncoveredWorkloadChecker struct {
-	Workload              models.WorkloadListItem
+	Workload              *models.Workload
 	Namespace             string
 	AuthorizationPolicies []*security_v1.AuthorizationPolicy
 }
diff --git a/business/checkers/workloads_checker.go b/business/checkers/workloads_checker.go
index 6283b4c74..3cef3946a 100644
--- a/business/checkers/workloads_checker.go
+++ b/business/checkers/workloads_checker.go
@@ -12,16 +12,16 @@ const WorkloadCheckerType = "workload"
 
 type WorkloadChecker struct {
 	AuthorizationPolicies []*security_v1.AuthorizationPolicy
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	Cluster               string
 }
 
 func (w WorkloadChecker) Check() models.IstioValidations {
 	validations := models.IstioValidations{}
 
-	for _, wls := range w.WorkloadsPerNamespace {
-		for _, wl := range wls.Workloads {
-			validations.MergeValidations(w.runChecks(wl, wls.Namespace))
+	for ns, workloads := range w.WorkloadsPerNamespace {
+		for _, wl := range workloads {
+			validations.MergeValidations(w.runChecks(wl, ns))
 		}
 	}
 
@@ -29,7 +29,7 @@ func (w WorkloadChecker) Check() models.IstioValidations {
 }
 
 // runChecks runs all the individual checks for a single workload and appends the result into validations.
-func (w WorkloadChecker) runChecks(workload models.WorkloadListItem, namespace string) models.IstioValidations {
+func (w WorkloadChecker) runChecks(workload *models.Workload, namespace string) models.IstioValidations {
 	wlName := workload.Name
 	key, rrValidation := EmptyValidValidation(wlName, namespace, schema.GroupVersionKind{Group: "", Version: "", Kind: WorkloadCheckerType}, w.Cluster)
 
diff --git a/business/istio_validations.go b/business/istio_validations.go
index 4c6f3099b..fc402fd59 100644
--- a/business/istio_validations.go
+++ b/business/istio_validations.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"maps"
 	"slices"
+	"strconv"
 	"strings"
 
 	networking_v1 "istio.io/client-go/pkg/apis/networking/v1"
@@ -18,6 +19,7 @@ import (
 	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes"
 	"github.com/kiali/kiali/kubernetes/cache"
+	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
 	"github.com/kiali/kiali/observability"
 	"github.com/kiali/kiali/prometheus/internalmetrics"
@@ -58,18 +60,9 @@ type ReferenceChecker interface {
 	References() models.IstioReferencesMap
 }
 
-func validationsForCluster(validations models.IstioValidations, cluster string) models.IstioValidations {
-	clusterValidations := models.IstioValidations{}
-	for validationKey, validation := range validations {
-		if validationKey.Cluster == cluster {
-			clusterValidations[validationKey] = validation
-		}
-	}
-	return clusterValidations
-}
-
 func (in *IstioValidationsService) GetValidations(ctx context.Context, cluster string) (models.IstioValidations, error) {
-	return validationsForCluster(in.kialiCache.Validations().Items(), cluster), nil
+	validations := in.kialiCache.Validations().Items()
+	return models.IstioValidations(validations).FilterByCluster(cluster), nil
 }
 
 func (in *IstioValidationsService) GetValidationsForNamespace(ctx context.Context, cluster, namespace string) (models.IstioValidations, error) {
@@ -101,7 +94,8 @@ func (in *IstioValidationsService) GetValidationsForService(ctx context.Context,
 		return nil, fmt.Errorf("service [namespace: %s] [name: %s] doesn't exist for Validations", namespace, service)
 	}
 
-	return validationsForCluster(in.kialiCache.Validations().Items(), cluster).FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "service"}, service), nil
+	validations := in.kialiCache.Validations().Items()
+	return models.IstioValidations(validations).FilterByCluster(cluster).FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "service"}, service), nil
 }
 
 func (in *IstioValidationsService) GetValidationsForWorkload(ctx context.Context, cluster, namespace, workload string) (models.IstioValidations, error) {
@@ -114,7 +108,8 @@ func (in *IstioValidationsService) GetValidationsForWorkload(ctx context.Context
 		return nil, err
 	}
 
-	return validationsForCluster(in.kialiCache.Validations().Items(), cluster).FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "workload"}, workload), nil
+	validations := in.kialiCache.Validations().Items()
+	return models.IstioValidations(validations).FilterByCluster(cluster).FilterBySingleType(schema.GroupVersionKind{Group: "", Version: "", Kind: "workload"}, workload), nil
 }
 
 type validationNamespaceInfo struct {
@@ -130,20 +125,60 @@ type validationClusterInfo struct {
 	registryServices []*kubernetes.RegistryService // registry services for the cluster (all namespaces)
 }
 
+// changeMap key values are determined by the validation logic, and typically identifies a config object,
+// or set of confg objects. The value is typically resourceVersion, or some equivalent.
+type ValidationChangeMap map[string]string
+
+func (in ValidationChangeMap) update(key, value string, report bool) bool {
+	prev, exists := in[key]
+
+	if !exists {
+		if report {
+			log.Tracef("validations: new config detected: %s", key)
+		}
+		in[key] = value
+		return true
+	}
+
+	if prev != value {
+		if report {
+			log.Tracef("validations: config change detected: %s", key)
+		}
+		in[key] = value
+		return true
+	}
+
+	return false
+}
+
 // validationInfo holds information gathered during a single validation reconciliation. It is used to hold information that
 // may otherwise need to be recalculated.
 type validationInfo struct {
 	// cross-cluster information
-	clusters []string                                  // all clusters being validated
-	mesh     *models.Mesh                              // control plane info
-	nsMap    map[string][]models.Namespace             // cluster => namespaces
-	saMap    map[string][]string                       // cluster => serviceAccounts
-	wlMap    map[string]map[string]models.WorkloadList // cluster => namespace => WorkloadList, all workloads
+	clusters []string                               // all clusters being validated
+	mesh     *models.Mesh                           // control plane info
+	nsMap    map[string][]models.Namespace          // cluster => namespaces
+	saMap    map[string][]string                    // cluster => serviceAccounts
+	wlMap    map[string]map[string]models.Workloads // cluster => namespace => []Workload, all workloads
 
 	// clusterInfo is reset for each cluster being validated
 	clusterInfo *validationClusterInfo
+
 	// nsInfo is reset for each namespace being validated (for the cluster being validated)
 	nsInfo *validationNamespaceInfo
+
+	// changeMap is used to store config ResourceVersion, or an equivalent. When supplied to
+	// NewValidationInfo() it sets changeDetection enabled.  It is expected to persist through
+	// multiple validation runs, and it is used to check for changes and eliminate checker runs
+	// when nothing significant has changed. If not supplied then no change detection is performed.
+	changeMap ValidationChangeMap
+
+	// hasBaseChange indicates whether a change is detected in the base data, likely meaning that
+	// we need a full validation pass, on each cluster
+	hasBaseChange bool
+
+	// reportChange is an internal flag for debugging, that logs keys that have a value change
+	reportChange bool
 }
 
 // NewValidationInfo returns an initialized validationInfo structure. This is not a "free" call, the initial structure is
@@ -151,7 +186,7 @@ type validationInfo struct {
 // a validation pass to hold "computed" information, and avoid performing the same work multiple times, when evaluating
 // different clusters, or different namespaces for a cluster. Initially unused structures/maps will be set to nil, and
 // arrays will be initialized to empty.
-func (in *IstioValidationsService) NewValidationInfo(ctx context.Context, clusters []string) (*validationInfo, error) {
+func (in *IstioValidationsService) NewValidationInfo(ctx context.Context, clusters []string, changeMap ValidationChangeMap) (*validationInfo, error) {
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "newValidationInfo",
 		observability.Attribute("package", "business"),
@@ -159,10 +194,11 @@ func (in *IstioValidationsService) NewValidationInfo(ctx context.Context, cluste
 	defer end()
 
 	vInfo := validationInfo{
-		clusters: clusters,
-		nsMap:    map[string][]models.Namespace{},
-		saMap:    map[string][]string{},
-		wlMap:    map[string]map[string]models.WorkloadList{},
+		changeMap: changeMap,
+		clusters:  clusters,
+		nsMap:     map[string][]models.Namespace{},
+		saMap:     map[string][]string{},
+		wlMap:     map[string]map[string]models.Workloads{},
 	}
 	mesh, err := in.mesh.discovery.Mesh(ctx)
 	if err != nil {
@@ -170,6 +206,7 @@ func (in *IstioValidationsService) NewValidationInfo(ctx context.Context, cluste
 	}
 	vInfo.mesh = mesh
 
+	// gather base info, mapped by cluster
 	for _, cluster := range clusters {
 		workloads, err := in.workload.GetAllWorkloads(ctx, cluster, "")
 		if err != nil {
@@ -186,12 +223,45 @@ func (in *IstioValidationsService) NewValidationInfo(ctx context.Context, cluste
 		vInfo.saMap[cluster] = in.getServiceAccounts(namespaces, vInfo.wlMap[cluster])
 	}
 
+	// if changeDetection is enabled then loop through the workloads, looking for a change
+	if changeMap != nil {
+		vInfo.reportChange = true
+
+		changeDetected := false
+		numWorkloads := 0
+		for _, workloadsMap := range vInfo.wlMap {
+			for _, workloads := range workloadsMap {
+				numWorkloads += len(workloads)
+				for _, w := range workloads {
+					changeDetected = changeMap.update(w.ValidationKey, w.ValidationVersion, vInfo.reportChange) || changeDetected
+				}
+			}
+		}
+		changeDetected = changeMap.update("validation-num-workloads", strconv.Itoa(numWorkloads), vInfo.reportChange) || changeDetected
+		vInfo.hasBaseChange = changeDetected
+	}
+
 	return &vInfo, nil
 }
 
-// Validate runs a full validation on all objects. It returns an IstioValidations object with all the checks found when running all
-// the enabled checkers.
-func (in *IstioValidationsService) Validate(ctx context.Context, cluster string, vInfo *validationInfo) (models.IstioValidations, error) {
+func (in *validationInfo) changeDetectionEnabled() bool {
+	return in.changeMap != nil
+}
+
+func (in *validationInfo) update(kind, cluster, namespace, name, value string) bool {
+	key := fmt.Sprintf("%s:%s:%s:%s", kind, cluster, namespace, name)
+	return in.changeMap.update(key, value, in.reportChange)
+}
+
+func (in *validationInfo) forceCheckers() bool {
+	return in.hasBaseChange
+}
+
+// Validate runs a full validation on all objects. The first return variable is the "validationPerformed" bool, indicating whether or not
+// the validation checkers were run. It will return false if a changeMap is provided in vInfo and no config changes were detected for the
+// cluster. Otherwise, it will return true. When true the new "validations" are returned in the second return variable. When false
+// the second argument is nil.
+func (in *IstioValidationsService) Validate(ctx context.Context, cluster string, vInfo *validationInfo) (bool, models.IstioValidations, error) {
 	var end observability.EndFunc
 	ctx, end = observability.StartSpan(ctx, "getValidations",
 		observability.Attribute("package", "business"),
@@ -204,7 +274,8 @@ func (in *IstioValidationsService) Validate(ctx context.Context, cluster string,
 
 	validations := models.IstioValidations{}
 	vInfo.clusterInfo = &validationClusterInfo{
-		cluster: cluster,
+		cluster:          cluster,
+		registryServices: []*kubernetes.RegistryService{},
 	}
 
 	if registryStatus := in.kialiCache.GetRegistryStatus(cluster); registryStatus != nil {
@@ -230,10 +301,18 @@ func (in *IstioValidationsService) Validate(ctx context.Context, cluster string,
 	}
 	istioConfigList, err := in.istioConfig.GetIstioConfigListForCluster(ctx, cluster, meta_v1.NamespaceAll, criteria)
 	if err != nil {
-		return nil, err
+		return false, nil, err
 	}
 	vInfo.clusterInfo.istioConfig = istioConfigList
 
+	// if change detection is enabled then decide if we need to run the checkers
+	if vInfo.changeDetectionEnabled() {
+		changeDetected := detectClusterConfigChange(vInfo)
+		if !changeDetected && !vInfo.forceCheckers() {
+			return false, nil, nil
+		}
+	}
+
 	for _, namespace := range vInfo.nsMap[cluster] {
 		vInfo.nsInfo = &validationNamespaceInfo{
 			namespace: &namespace,
@@ -241,43 +320,132 @@ func (in *IstioValidationsService) Validate(ctx context.Context, cluster string,
 
 		err := in.setNamespaceIstioConfig(vInfo)
 		if err != nil {
-			return nil, err
+			return false, nil, err
 		}
 
 		if err := in.setNonLocalMTLSConfig(vInfo); err != nil {
-			return nil, err
+			return false, nil, err
 		}
 
 		objectCheckers := in.getAllObjectCheckers(vInfo)
 
-		// Get group validations for same kind istio objects
 		validations.MergeValidations(runObjectCheckers(objectCheckers))
 	}
 
-	return validations, nil
+	return true, validations, nil
 }
 
-// toWorkloadMap takes a list of workloads from different namespaces, and returns a map: namespace => WorkloadList
-func toWorkloadMap(workloads models.Workloads) map[string]models.WorkloadList {
-	workloadMap := map[string]models.WorkloadList{}
+// toWorkloadMap takes a list of workloads from different namespaces, and returns a map: namespace => models.Workloads
+func toWorkloadMap(workloads models.Workloads) map[string]models.Workloads {
+	workloadMap := map[string]models.Workloads{}
 
 	for _, w := range workloads {
-		wItem := &models.WorkloadListItem{Health: *models.EmptyWorkloadHealth()}
-		wItem.ParseWorkload(w)
-		workloadList, ok := workloadMap[w.Namespace]
+		workloads, ok := workloadMap[w.Namespace]
 		if ok {
-			workloadList.Workloads = append(workloadList.Workloads, *wItem)
+			workloads = append(workloads, w)
 		} else {
-			workloadList = models.WorkloadList{
-				Namespace: w.Namespace,
-				Workloads: []models.WorkloadListItem{*wItem},
-			}
+			workloads = models.Workloads{w}
 		}
-		workloadMap[w.Namespace] = workloadList
+		workloadMap[w.Namespace] = workloads
 	}
 	return workloadMap
 }
 
+// detectClusterConfigChange checks the version values for all of the relevant cluster config, updating
+// as needed, and returns whether a change was detected.
+func detectClusterConfigChange(vInfo *validationInfo) bool {
+	change := false
+	cluster := vInfo.clusterInfo.cluster
+
+	// loop through the services and gather up their resourceVersions
+	for _, s := range vInfo.clusterInfo.registryServices {
+		change = vInfo.update("SV", cluster, s.Attributes.Namespace, s.Attributes.Name, s.ResourceVersion) || change
+	}
+
+	// loop through the config and gather up their resourceVersions
+	config := vInfo.clusterInfo.istioConfig
+	for _, c := range config.AuthorizationPolicies {
+		change = vInfo.update("AP", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.DestinationRules {
+		change = vInfo.update("DR", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.EnvoyFilters {
+		change = vInfo.update("EF", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.Gateways {
+		change = vInfo.update("GW", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.K8sGateways {
+		change = vInfo.update("KG", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.K8sGRPCRoutes {
+		change = vInfo.update("KGRPC", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.K8sHTTPRoutes {
+		change = vInfo.update("KHTTP", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.K8sReferenceGrants {
+		change = vInfo.update("KRG", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.K8sTCPRoutes {
+		change = vInfo.update("KTCP", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.K8sTLSRoutes {
+		change = vInfo.update("KTLS", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.PeerAuthentications {
+		change = vInfo.update("PA", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.RequestAuthentications {
+		change = vInfo.update("RA", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.ServiceEntries {
+		change = vInfo.update("SE", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.Sidecars {
+		change = vInfo.update("SC", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.Telemetries {
+		change = vInfo.update("TE", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.VirtualServices {
+		change = vInfo.update("VS", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.WasmPlugins {
+		change = vInfo.update("WP", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.WorkloadEntries {
+		change = vInfo.update("WE", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+	for _, c := range config.WorkloadGroups {
+		change = vInfo.update("WG", cluster, c.Namespace, c.Name, c.ResourceVersion) || change
+	}
+
+	numConfig := len(config.AuthorizationPolicies) +
+		len(config.DestinationRules) +
+		len(config.EnvoyFilters) +
+		len(config.Gateways) +
+		len(config.K8sGateways) +
+		len(config.K8sGRPCRoutes) +
+		len(config.K8sHTTPRoutes) +
+		len(config.K8sReferenceGrants) +
+		len(config.K8sTCPRoutes) +
+		len(config.K8sTLSRoutes) +
+		len(config.PeerAuthentications) +
+		len(config.RequestAuthentications) +
+		len(config.ServiceEntries) +
+		len(config.Sidecars) +
+		len(config.Telemetries) +
+		len(config.VirtualServices) +
+		len(config.WorkloadEntries) +
+		len(config.WorkloadGroups) +
+		len(config.WasmPlugins)
+	change = vInfo.update("validation-num-config", cluster, "", "", strconv.Itoa(numConfig)) || change
+
+	return change
+}
+
 // getAllObjectCheckers returns all of the checkers to be executed for a full validation.
 // TODO: we may want to to pass vInfo into all of these, if the checkers themselves are re-computing information
 func (in *IstioValidationsService) getAllObjectCheckers(vInfo *validationInfo) []checkers.ObjectChecker {
@@ -336,7 +504,7 @@ func (in *IstioValidationsService) ValidateIstioObject(ctx context.Context, clus
 	defer timer.ObserveDuration()
 
 	// validating a single object is not particularly efficient, it still requires a lot of up-front setup
-	vInfo, err := in.NewValidationInfo(ctx, in.namespace.GetClusterList())
+	vInfo, err := in.NewValidationInfo(ctx, in.namespace.GetClusterList(), nil)
 	if err != nil {
 		return nil, models.IstioReferencesMap{}, err
 	}
@@ -523,16 +691,16 @@ func runObjectReferenceChecker(referenceChecker ReferenceChecker) models.IstioRe
 // getServiceAccounts gets SA information given the namespaces and workloads for a given cluster.
 func (in *IstioValidationsService) getServiceAccounts(
 	namespaces []models.Namespace,
-	workloadsMap map[string]models.WorkloadList,
+	workloadsMap map[string]models.Workloads,
 ) []string {
 	serviceAccounts := map[string]bool{}
 	istioDomain := strings.Replace(config.Get().ExternalServices.Istio.IstioIdentityDomain, "svc.", "", 1)
 
 	for _, ns := range namespaces {
 		saFullNameNs := fmt.Sprintf("%s/ns/%s/sa/", istioDomain, ns.Name)
-		workloadList := workloadsMap[ns.Name]
-		for _, wl := range workloadList.Workloads {
-			for _, sAccountName := range wl.ServiceAccountNames {
+		workloads := workloadsMap[ns.Name]
+		for _, w := range workloads {
+			for _, sAccountName := range w.ServiceAccountNames {
 				saFullName := saFullNameNs + sAccountName
 				serviceAccounts[saFullName] = true
 			}
diff --git a/business/references/auth_policy_references.go b/business/references/auth_policy_references.go
index bde8bdd7a..71e305bc4 100644
--- a/business/references/auth_policy_references.go
+++ b/business/references/auth_policy_references.go
@@ -18,7 +18,7 @@ type AuthorizationPolicyReferences struct {
 	ServiceEntries        []*networking_v1.ServiceEntry
 	VirtualServices       []*networking_v1.VirtualService
 	RegistryServices      []*kubernetes.RegistryService
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 }
 
 func (n AuthorizationPolicyReferences) References() models.IstioReferencesMap {
@@ -96,14 +96,14 @@ func (n AuthorizationPolicyReferences) getWorkloadReferences(ap *security_v1.Aut
 		selector := labels.SelectorFromSet(ap.Spec.Selector.MatchLabels)
 
 		// AuthPolicy searches Workloads from own namespace, or from all namespaces when AuthPolicy is in root namespace
-		for _, wls := range n.WorkloadsPerNamespace {
-			if !config.IsRootNamespace(ap.Namespace) && wls.Namespace != ap.Namespace {
+		for ns, workloads := range n.WorkloadsPerNamespace {
+			if !config.IsRootNamespace(ap.Namespace) && ns != ap.Namespace {
 				continue
 			}
-			for _, wl := range wls.Workloads {
+			for _, wl := range workloads {
 				wlLabelSet := labels.Set(wl.Labels)
 				if selector.Matches(wlLabelSet) {
-					result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: wls.Namespace})
+					result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: ns})
 				}
 			}
 		}
diff --git a/business/references/destination_rule_references.go b/business/references/destination_rule_references.go
index 4667a481f..04697af7c 100644
--- a/business/references/destination_rule_references.go
+++ b/business/references/destination_rule_references.go
@@ -15,7 +15,7 @@ type DestinationRuleReferences struct {
 	Namespaces            models.Namespaces
 	DestinationRules      []*networking_v1.DestinationRule
 	VirtualServices       []*networking_v1.VirtualService
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 	ServiceEntries        []*networking_v1.ServiceEntry
 	RegistryServices      []*kubernetes.RegistryService
 }
@@ -84,11 +84,11 @@ func (n DestinationRuleReferences) getWorkloadReferences(dr *networking_v1.Desti
 			subsetLabelSet := labels.Set(subset.Labels)
 			subsetSelector := labels.SelectorFromSet(subsetLabelSet)
 
-			for _, wl := range n.WorkloadsPerNamespace[localNs].Workloads {
-				wlLabelSet := labels.Set(wl.Labels)
+			for _, w := range n.WorkloadsPerNamespace[localNs] {
+				wlLabelSet := labels.Set(w.Labels)
 				if selector.Matches(wlLabelSet) {
 					if subsetSelector.Matches(wlLabelSet) {
-						allWorkloads = append(allWorkloads, models.WorkloadReference{Name: wl.Name, Namespace: localNs})
+						allWorkloads = append(allWorkloads, models.WorkloadReference{Name: w.Name, Namespace: localNs})
 					}
 				}
 			}
diff --git a/business/references/gateway_references.go b/business/references/gateway_references.go
index fac70d040..e79e6171e 100644
--- a/business/references/gateway_references.go
+++ b/business/references/gateway_references.go
@@ -13,7 +13,7 @@ import (
 type GatewayReferences struct {
 	Gateways              []*networking_v1.Gateway
 	VirtualServices       []*networking_v1.VirtualService
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 }
 
 func (n GatewayReferences) References() models.IstioReferencesMap {
@@ -38,11 +38,11 @@ func (n GatewayReferences) getWorkloadReferences(gw *networking_v1.Gateway) []mo
 	selector := labels.SelectorFromSet(gw.Spec.Selector)
 
 	// Gateway searches Workloads from all namespace
-	for _, wls := range n.WorkloadsPerNamespace {
-		for _, wl := range wls.Workloads {
+	for ns, workloads := range n.WorkloadsPerNamespace {
+		for _, wl := range workloads {
 			wlLabelSet := labels.Set(wl.Labels)
 			if selector.Matches(wlLabelSet) {
-				result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: wls.Namespace})
+				result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: ns})
 			}
 		}
 	}
diff --git a/business/references/k8s_gateway_references.go b/business/references/k8s_gateway_references.go
index 7940ab6e7..34390851f 100644
--- a/business/references/k8s_gateway_references.go
+++ b/business/references/k8s_gateway_references.go
@@ -12,7 +12,7 @@ type K8sGatewayReferences struct {
 	K8sGateways           []*k8s_networking_v1.Gateway
 	K8sHTTPRoutes         []*k8s_networking_v1.HTTPRoute
 	K8sGRPCRoutes         []*k8s_networking_v1.GRPCRoute
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 }
 
 func (g K8sGatewayReferences) References() models.IstioReferencesMap {
@@ -33,9 +33,9 @@ func (g K8sGatewayReferences) getWorkloadReferences(gw *k8s_networking_v1.Gatewa
 	result := make([]models.WorkloadReference, 0)
 
 	// Gateway searches Workloads from own namespace and Gateway Label
-	for _, wl := range g.WorkloadsPerNamespace[gw.Namespace].Workloads {
-		if gw.Name == wl.Labels[config.Get().IstioLabels.AmbientWaypointGatewayLabel] {
-			result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: gw.Namespace})
+	for _, w := range g.WorkloadsPerNamespace[gw.Namespace] {
+		if gw.Name == w.Labels[config.Get().IstioLabels.AmbientWaypointGatewayLabel] {
+			result = append(result, models.WorkloadReference{Name: w.Name, Namespace: gw.Namespace})
 		}
 	}
 	return result
diff --git a/business/references/peer_auth_references.go b/business/references/peer_auth_references.go
index 6a0157eb7..9d1530e2e 100644
--- a/business/references/peer_auth_references.go
+++ b/business/references/peer_auth_references.go
@@ -13,7 +13,7 @@ import (
 
 type PeerAuthReferences struct {
 	MTLSDetails           kubernetes.MTLSDetails
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 }
 
 func (n PeerAuthReferences) References() models.IstioReferencesMap {
@@ -101,10 +101,10 @@ func (n PeerAuthReferences) getWorkloadReferences(pa *security_v1.PeerAuthentica
 		selector := labels.SelectorFromSet(pa.Spec.Selector.MatchLabels)
 
 		// PeerAuth searches Workloads from own namespace
-		for _, wl := range n.WorkloadsPerNamespace[pa.Namespace].Workloads {
-			wlLabelSet := labels.Set(wl.Labels)
+		for _, w := range n.WorkloadsPerNamespace[pa.Namespace] {
+			wlLabelSet := labels.Set(w.Labels)
 			if selector.Matches(wlLabelSet) {
-				result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: pa.Namespace})
+				result = append(result, models.WorkloadReference{Name: w.Name, Namespace: pa.Namespace})
 			}
 		}
 	}
diff --git a/business/references/sidecar_references.go b/business/references/sidecar_references.go
index 9d39aebb1..4aa7a1c13 100644
--- a/business/references/sidecar_references.go
+++ b/business/references/sidecar_references.go
@@ -18,7 +18,7 @@ type SidecarReferences struct {
 	Namespaces            models.Namespaces
 	ServiceEntries        []*networking_v1.ServiceEntry
 	RegistryServices      []*kubernetes.RegistryService
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 }
 
 func (n SidecarReferences) References() models.IstioReferencesMap {
@@ -106,10 +106,10 @@ func (n SidecarReferences) getWorkloadReferences(sc *networking_v1.Sidecar) []mo
 		selector := labels.SelectorFromSet(sc.Spec.WorkloadSelector.Labels)
 
 		// Sidecar searches Workloads from own namespace
-		for _, wl := range n.WorkloadsPerNamespace[sc.Namespace].Workloads {
-			wlLabelSet := labels.Set(wl.Labels)
+		for _, w := range n.WorkloadsPerNamespace[sc.Namespace] {
+			wlLabelSet := labels.Set(w.Labels)
 			if selector.Matches(wlLabelSet) {
-				result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: sc.Namespace})
+				result = append(result, models.WorkloadReference{Name: w.Name, Namespace: sc.Namespace})
 			}
 		}
 	}
diff --git a/business/references/workloadgroup_references.go b/business/references/workloadgroup_references.go
index c8446ea15..b387ba524 100644
--- a/business/references/workloadgroup_references.go
+++ b/business/references/workloadgroup_references.go
@@ -12,7 +12,7 @@ import (
 type WorkloadGroupReferences struct {
 	WorkloadGroups        []*networking_v1.WorkloadGroup
 	WorkloadEntries       []*networking_v1.WorkloadEntry
-	WorkloadsPerNamespace map[string]models.WorkloadList
+	WorkloadsPerNamespace map[string]models.Workloads
 }
 
 func (n WorkloadGroupReferences) References() models.IstioReferencesMap {
@@ -33,10 +33,10 @@ func (n WorkloadGroupReferences) getWorkloadReferences(wg *networking_v1.Workloa
 	result := make([]models.WorkloadReference, 0)
 
 	// Searches Workloads from all namespace
-	for _, wls := range n.WorkloadsPerNamespace {
-		for _, wl := range wls.Workloads {
-			if wg.Namespace == wls.Namespace && wg.Name == wl.Name {
-				result = append(result, models.WorkloadReference{Name: wl.Name, Namespace: wls.Namespace})
+	for ns, workloads := range n.WorkloadsPerNamespace {
+		for _, w := range workloads {
+			if wg.Namespace == ns && wg.Name == w.Name {
+				result = append(result, models.WorkloadReference{Name: w.Name, Namespace: ns})
 			}
 		}
 	}
diff --git a/business/services.go b/business/services.go
index 2bbb28960..b6935e9e5 100644
--- a/business/services.go
+++ b/business/services.go
@@ -581,11 +581,12 @@ func (in *SvcService) GetServiceDetails(ctx context.Context, cluster, namespace,
 		return nil, err
 	}
 
+	config := config.Get()
 	wo := models.WorkloadOverviews{}
 	isAmbient := len(ws) > 0
 	for _, w := range ws {
 		wi := &models.WorkloadListItem{}
-		wi.ParseWorkload(w)
+		wi.ParseWorkload(w, config)
 		wo = append(wo, wi)
 		// The service is not marked as Ambient if any of the workloads is Ambient
 		if !w.IsAmbient {
diff --git a/business/workloads.go b/business/workloads.go
index 0856fe2d6..7d5d3fd46 100644
--- a/business/workloads.go
+++ b/business/workloads.go
@@ -8,6 +8,7 @@ import (
 	"io"
 	"net/http"
 	"regexp"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -148,7 +149,7 @@ func (in *WorkloadService) isWorkloadValid(workloadType string) bool {
 }
 
 // @TODO do validations per cluster
-func (in *WorkloadService) getWorkloadValidations(authpolicies []*security_v1.AuthorizationPolicy, workloadsPerNamespace map[string]models.WorkloadList) models.IstioValidations {
+func (in *WorkloadService) getWorkloadValidations(authpolicies []*security_v1.AuthorizationPolicy, workloadsPerNamespace map[string]models.Workloads) models.IstioValidations {
 	validations := checkers.WorkloadChecker{
 		AuthorizationPolicies: authpolicies,
 		WorkloadsPerNamespace: workloadsPerNamespace,
@@ -283,7 +284,7 @@ func (in *WorkloadService) GetWorkloadList(ctx context.Context, criteria Workloa
 
 	for _, w := range ws {
 		wItem := &models.WorkloadListItem{Health: *models.EmptyWorkloadHealth()}
-		wItem.ParseWorkload(w)
+		wItem.ParseWorkload(w, in.config)
 		if istioConfigList, ok := istioConfigMap[cluster]; ok && criteria.IncludeIstioResources {
 			wItem.IstioReferences = FilterUniqueIstioReferences(FilterWorkloadReferences(in.config, wItem.Labels, istioConfigList, cluster))
 		}
@@ -301,8 +302,8 @@ func (in *WorkloadService) GetWorkloadList(ctx context.Context, criteria Workloa
 	for _, istioConfigList := range istioConfigMap {
 		// @TODO multi cluster validations
 		authpolicies := istioConfigList.AuthorizationPolicies
-		allWorkloads := map[string]models.WorkloadList{}
-		allWorkloads[namespace] = *workloadList
+		allWorkloads := map[string]models.Workloads{}
+		allWorkloads[namespace] = ws
 		validations := in.getWorkloadValidations(authpolicies, allWorkloads)
 		validations.StripIgnoredChecks()
 		workloadList.Validations = workloadList.Validations.MergeValidations(validations)
@@ -1426,19 +1427,26 @@ func (in *WorkloadService) fetchWorkloadsFromCluster(ctx context.Context, cluste
 			w.SetPods(cPods)
 		}
 
-		// Add the Proxy Status to the workload
-		for _, pod := range w.Pods {
-			isWaypoint := w.IsWaypoint()
-			if config.Get().ExternalServices.Istio.IstioAPIEnabled && (pod.HasIstioSidecar() || isWaypoint) {
-				pod.ProxyStatus = in.businessLayer.ProxyStatus.GetPodProxyStatus(cluster, namespace, pod.Name, !isWaypoint)
-			}
+		if cnFound {
 			// Add the Proxy Status to the workload
-			if pod.AmbientEnabled() {
-				w.WaypointWorkloads = in.GetWaypointsForWorkload(ctx, *w, false)
+			for _, pod := range w.Pods {
+				isWaypoint := w.IsWaypoint()
+				if in.config.ExternalServices.Istio.IstioAPIEnabled && (pod.HasIstioSidecar() || isWaypoint) {
+					pod.ProxyStatus = in.businessLayer.ProxyStatus.GetPodProxyStatus(cluster, namespace, pod.Name, !isWaypoint)
+				}
+				// Add the Proxy Status to the workload
+				if pod.AmbientEnabled() {
+					w.WaypointWorkloads = in.GetWaypointsForWorkload(ctx, *w, false)
+				}
 			}
-		}
 
-		if cnFound {
+			// Add some precalculated fields useful for validation
+			w.ValidationKey = fmt.Sprintf("%s:%s:%s", w.Cluster, w.Namespace, w.Name)
+
+			w.ServiceAccountNames = w.Pods.ServiceAccounts()
+			slices.Sort(w.ServiceAccountNames)
+			w.ValidationVersion = fmt.Sprintf("%v:%v", w.Labels, w.ServiceAccountNames)
+
 			ws = append(ws, w)
 		}
 	}
@@ -2113,7 +2121,7 @@ func (in *WorkloadService) fetchWorkload(ctx context.Context, criteria WorkloadC
 
 		// Add the Proxy Status to the workload
 		for _, pod := range w.Pods {
-			if config.Get().ExternalServices.Istio.IstioAPIEnabled && (pod.HasIstioSidecar() || isWaypoint) {
+			if in.config.ExternalServices.Istio.IstioAPIEnabled && (pod.HasIstioSidecar() || isWaypoint) {
 				pod.ProxyStatus = in.businessLayer.ProxyStatus.GetPodProxyStatus(criteria.Cluster, criteria.Namespace, pod.Name, !isWaypoint)
 			}
 			// If Ambient is enabled for pod, check if has any Waypoint proxy
@@ -2170,7 +2178,7 @@ func (in *WorkloadService) GetWaypoints(ctx context.Context) models.Workloads {
 		return in.cache.GetWaypointList()
 	}
 
-	waypoints := []*models.Workload{}
+	waypoints := models.Workloads{}
 
 	for cluster := range in.userClients {
 		gateways, err := in.GetAllWorkloads(ctx, cluster, config.GatewayLabel)
diff --git a/config/config.go b/config/config.go
index 53d8b4f4b..0a5232987 100644
--- a/config/config.go
+++ b/config/config.go
@@ -321,10 +321,11 @@ type IstioConfig struct {
 	IstiodPodMonitoringPort           int               `yaml:"istiod_pod_monitoring_port,omitempty"`
 	// IstiodPollingIntervalSeconds is how often in seconds Kiali will poll istiod(s) for
 	// proxy status and registry services. Polling is not performed if IstioAPIEnabled is false.
-	IstiodPollingIntervalSeconds int             `yaml:"istiod_polling_interval_seconds,omitempty"`
-	Registry                     *RegistryConfig `yaml:"registry,omitempty"`
-	RootNamespace                string          `yaml:"root_namespace,omitempty"`
-	UrlServiceVersion            string          `yaml:"url_service_version"`
+	IstiodPollingIntervalSeconds     int             `yaml:"istiod_polling_interval_seconds,omitempty"`
+	Registry                         *RegistryConfig `yaml:"registry,omitempty"`
+	RootNamespace                    string          `yaml:"root_namespace,omitempty"`
+	UrlServiceVersion                string          `yaml:"url_service_version"`
+	ValidationChangeDetectionEnabled bool            `yaml:"validation_change_detection_enabled,omitempty"`
 	// ValidationReconcileInterval sets how often Kiali will validate Istio configuration.
 	// Validations cannot be disabled at the moment but you can set this to a long period of time.
 	ValidationReconcileInterval *time.Duration `yaml:"validation_reconcile_interval,omitempty"`
@@ -758,6 +759,7 @@ func NewConfig() (c *Config) {
 				IstiodPollingIntervalSeconds:      20,
 				RootNamespace:                     "istio-system",
 				UrlServiceVersion:                 "",
+				ValidationChangeDetectionEnabled:  true,
 				ValidationReconcileInterval:       util.AsPtr(time.Minute),
 				GatewayAPIClasses:                 []GatewayAPIClass{},
 			},
diff --git a/controller/validations.go b/controller/validations.go
index 374fc524e..007e58944 100644
--- a/controller/validations.go
+++ b/controller/validations.go
@@ -14,6 +14,7 @@ import (
 	ctrlsource "sigs.k8s.io/controller-runtime/pkg/source"
 
 	"github.com/kiali/kiali/business"
+	"github.com/kiali/kiali/config"
 	"github.com/kiali/kiali/kubernetes/cache"
 	"github.com/kiali/kiali/log"
 	"github.com/kiali/kiali/models"
@@ -114,30 +115,48 @@ func (r *ValidationsReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 
 	// Check version before performing replace.
 	version := r.kialiCache.Validations().Version()
-	allClusterValidations := make(models.IstioValidations)
+	newValidations := make(models.IstioValidations)
+	prevValidations := r.kialiCache.Validations().Items()
+
+	// If enabled, the same changeMap is used on each Reconcile. It holds the "resource version" of
+	// each object used in the validation, and is then used to look for version changes (or a change in
+	// the number of objects).  We keep it in the cache for sane access.
+	var changeMap business.ValidationChangeMap
+	if config.Get().ExternalServices.Istio.ValidationChangeDetectionEnabled {
+		changeMap = r.kialiCache.ValidationConfig().Items()
+	}
 
 	// validation requires cross-cluster service account information.
-	vInfo, err := r.validationsService.NewValidationInfo(ctx, r.clusters)
+	vInfo, err := r.validationsService.NewValidationInfo(ctx, r.clusters, changeMap)
 	if err != nil {
 		log.Errorf("[ValidationsReconciler] Error creating validation info: %s", err)
 		return ctrl.Result{}, err
 	}
 
 	for _, cluster := range r.clusters {
-		clusterValidations, err := r.validationsService.Validate(ctx, cluster, vInfo)
+		validationPerformed, clusterValidations, err := r.validationsService.Validate(ctx, cluster, vInfo)
 		if err != nil {
-			log.Errorf("[ValidationsReconciler] Error creating validations for cluster %s: %s", cluster, err)
+			log.Errorf("[ValidationsReconciler] Error performing validation for cluster %s: %s", cluster, err)
 			return ctrl.Result{}, err
 		}
 
-		allClusterValidations = allClusterValidations.MergeValidations(clusterValidations)
+		// if there have been no config changes for the cluster, just re-use the prior validations
+		if !validationPerformed {
+			log.Tracef("validations: no changes for cluster [%s], re-using", cluster)
+			clusterValidations = models.IstioValidations(prevValidations).FilterByCluster(cluster)
+		} else {
+			log.Tracef("validations: config changes found for cluster [%s], updating", cluster)
+		}
+
+		newValidations = newValidations.MergeValidations(clusterValidations)
 	}
 
 	if r.kialiCache.Validations().Version() != version {
 		return ctrl.Result{}, fmt.Errorf("validations have been updated since reconciling started. Requeuing validation")
 	}
 
-	r.kialiCache.Validations().Replace(allClusterValidations)
+	r.kialiCache.Validations().Replace(newValidations)
+	r.kialiCache.ValidationConfig().Replace(changeMap)
 
 	return ctrl.Result{}, nil
 }
diff --git a/frontend/src/types/IstioConfigList.ts b/frontend/src/types/IstioConfigList.ts
index ce81538d8..6bd3015bf 100644
--- a/frontend/src/types/IstioConfigList.ts
+++ b/frontend/src/types/IstioConfigList.ts
@@ -286,8 +286,8 @@ export const vsToIstioItems = (
   cluster?: string
 ): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
-  const hasValidations = (vKey: string): ObjectValidation =>
-    validations.virtualservice && validations.virtualservice[vKey];
+  const objectGVK = getGVKTypeString(gvkType.VirtualService);
+  const hasValidations = (vKey: string): ObjectValidation => validations[objectGVK] && validations[objectGVK][vKey];
 
   vss.forEach(vs => {
     const vKey = validationKey(vs.metadata.name, vs.metadata.namespace);
@@ -301,7 +301,7 @@ export const vsToIstioItems = (
       creationTimestamp: vs.metadata.creationTimestamp,
       resource: vs,
       resourceVersion: vs.metadata.resourceVersion,
-      validation: hasValidations(vKey) ? validations.virtualservice[vKey] : undefined
+      validation: hasValidations(vKey) ? validations[objectGVK][vKey] : undefined
     };
 
     istioItems.push(item);
@@ -316,8 +316,8 @@ export const drToIstioItems = (
   cluster?: string
 ): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
-  const hasValidations = (vKey: string): ObjectValidation =>
-    validations.destinationrule && validations.destinationrule[vKey];
+  const objectGVK = getGVKTypeString(gvkType.DestinationRule);
+  const hasValidations = (vKey: string): ObjectValidation => validations[objectGVK] && validations[objectGVK][vKey];
 
   drs.forEach(dr => {
     const vKey = validationKey(dr.metadata.name, dr.metadata.namespace);
@@ -331,7 +331,7 @@ export const drToIstioItems = (
       creationTimestamp: dr.metadata.creationTimestamp,
       resource: dr,
       resourceVersion: dr.metadata.resourceVersion,
-      validation: hasValidations(vKey) ? validations.destinationrule[vKey] : undefined
+      validation: hasValidations(vKey) ? validations[objectGVK][vKey] : undefined
     };
 
     istioItems.push(item);
@@ -347,7 +347,8 @@ export const gwToIstioItems = (
   cluster?: string
 ): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
-  const hasValidations = (vKey: string): ObjectValidation => validations.gateway && validations.gateway[vKey];
+  const objectGVK = getGVKTypeString(gvkType.Gateway);
+  const hasValidations = (vKey: string): ObjectValidation => validations[objectGVK] && validations[objectGVK][vKey];
   const vsGateways = new Set();
 
   vss.forEach(vs => {
@@ -373,7 +374,7 @@ export const gwToIstioItems = (
         creationTimestamp: gw.metadata.creationTimestamp,
         resource: gw,
         resourceVersion: gw.metadata.resourceVersion,
-        validation: hasValidations(vKey) ? validations.gateway[vKey] : undefined
+        validation: hasValidations(vKey) ? validations[objectGVK][vKey] : undefined
       };
 
       istioItems.push(item);
@@ -392,7 +393,8 @@ export const k8sGwToIstioItems = (
   gatewayLabel?: string
 ): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
-  const hasValidations = (vKey: string): ObjectValidation => validations.k8sgateway && validations.k8sgateway[vKey];
+  const objectGVK = getGVKTypeString(gvkType.K8sGateway);
+  const hasValidations = (vKey: string): ObjectValidation => validations[objectGVK] && validations[objectGVK][vKey];
   const k8sGateways = new Set();
 
   k8srs.forEach(k8sr => {
@@ -430,7 +432,7 @@ export const k8sGwToIstioItems = (
         creationTimestamp: gw.metadata.creationTimestamp,
         resource: gw,
         resourceVersion: gw.metadata.resourceVersion,
-        validation: hasValidations(vKey) ? validations.k8sgateway[vKey] : undefined
+        validation: hasValidations(vKey) ? validations[objectGVK][vKey] : undefined
       };
 
       istioItems.push(item);
@@ -442,7 +444,8 @@ export const k8sGwToIstioItems = (
 
 export const seToIstioItems = (see: ServiceEntry[], validations: Validations, cluster?: string): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
-  const hasValidations = (vKey: string): ObjectValidation => validations.serviceentry && validations.serviceentry[vKey];
+  const objectGVK = getGVKTypeString(gvkType.ServiceEntry);
+  const hasValidations = (vKey: string): ObjectValidation => validations[objectGVK] && validations[objectGVK][vKey];
 
   see.forEach(se => {
     const vKey = validationKey(se.metadata.name, se.metadata.namespace);
@@ -456,7 +459,7 @@ export const seToIstioItems = (see: ServiceEntry[], validations: Validations, cl
       creationTimestamp: se.metadata.creationTimestamp,
       resource: se,
       resourceVersion: se.metadata.resourceVersion,
-      validation: hasValidations(vKey) ? validations.serviceentry[vKey] : undefined
+      validation: hasValidations(vKey) ? validations[objectGVK][vKey] : undefined
     };
 
     istioItems.push(item);
@@ -471,7 +474,8 @@ export const k8sHTTPRouteToIstioItems = (
   cluster?: string
 ): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
-  const hasValidations = (vKey: string): ObjectValidation => validations.k8shttproute && validations.k8shttproute[vKey];
+  const objectGVK = getGVKTypeString(gvkType.K8sHTTPRoute);
+  const hasValidations = (vKey: string): ObjectValidation => validations[objectGVK] && validations[objectGVK][vKey];
 
   routes.forEach(route => {
     const vKey = validationKey(route.metadata.name, route.metadata.namespace);
@@ -485,7 +489,7 @@ export const k8sHTTPRouteToIstioItems = (
       creationTimestamp: route.metadata.creationTimestamp,
       resource: route,
       resourceVersion: route.metadata.resourceVersion,
-      validation: hasValidations(vKey) ? validations.k8shttproute[vKey] : undefined
+      validation: hasValidations(vKey) ? validations[objectGVK][vKey] : undefined
     };
 
     istioItems.push(item);
@@ -500,7 +504,8 @@ export const k8sGRPCRouteToIstioItems = (
   cluster?: string
 ): IstioConfigItem[] => {
   const istioItems: IstioConfigItem[] = [];
-  const hasValidations = (vKey: string): ObjectValidation => validations.k8sgrpcroute && validations.k8sgrpcroute[vKey];
+  const objectGVK = getGVKTypeString(gvkType.K8sGRPCRoute);
+  const hasValidations = (vKey: string): ObjectValidation => validations[objectGVK] && validations[objectGVK][vKey];
 
   grpcRoutes.forEach(route => {
     const vKey = validationKey(route.metadata.name, route.metadata.namespace);
@@ -514,7 +519,7 @@ export const k8sGRPCRouteToIstioItems = (
       creationTimestamp: route.metadata.creationTimestamp,
       resource: route,
       resourceVersion: route.metadata.resourceVersion,
-      validation: hasValidations(vKey) ? validations.k8sgrpcroute[vKey] : undefined
+      validation: hasValidations(vKey) ? validations[objectGVK][vKey] : undefined
     };
 
     istioItems.push(item);
diff --git a/kubernetes/cache/cache.go b/kubernetes/cache/cache.go
index 5a62ffd90..edac8e69b 100644
--- a/kubernetes/cache/cache.go
+++ b/kubernetes/cache/cache.go
@@ -79,9 +79,12 @@ type KialiCache interface {
 	ProxyStatusCache
 	ZtunnelDumpCache
 
-	// SetValidations caches validations for a cluster/namespace.
+	// Validations caches validations for a cluster/namespace.
 	Validations() store.Store[models.IstioValidationKey, *models.IstioValidation]
 
+	// ValidationWatcher stores values used for detecting changes in config used for validation
+	ValidationConfig() store.Store[string, string]
+
 	// SetClusters sets the list of clusters that the cache knows about.
 	SetClusters([]models.KubeCluster)
 
@@ -131,7 +134,8 @@ type kialiCacheImpl struct {
 
 	waypointList models.WaypointStore
 	// validations key'd by the validation key
-	validations store.Store[models.IstioValidationKey, *models.IstioValidation]
+	validations      store.Store[models.IstioValidationKey, *models.IstioValidation]
+	validationConfig store.Store[string, string]
 
 	// Info about the kube clusters that the cache knows about.
 	clusters    []models.KubeCluster
@@ -148,6 +152,7 @@ func NewKialiCache(kialiSAClients map[string]kubernetes.ClientInterface, cfg con
 		conf:                    cfg,
 		kubeCache:               make(map[string]KubeCache),
 		validations:             store.New[models.IstioValidationKey, *models.IstioValidation](),
+		validationConfig:        store.New[string, string](),
 		meshStore:               store.NewExpirationStore(ctx, store.New[string, *models.Mesh](), util.AsPtr(meshExpirationTime), nil),
 		namespaceStore:          store.NewExpirationStore(ctx, store.New[namespacesKey, map[string]models.Namespace](), &namespaceKeyTTL, nil),
 		refreshDuration:         time.Duration(cfg.KubernetesConfig.CacheDuration) * time.Second,
@@ -256,6 +261,10 @@ func (c *kialiCacheImpl) Validations() store.Store[models.IstioValidationKey, *m
 	return c.validations
 }
 
+func (c *kialiCacheImpl) ValidationConfig() store.Store[string, string] {
+	return c.validationConfig
+}
+
 // IsAmbientEnabled checks if the istio Ambient profile was enabled
 // by checking if the ztunnel daemonset exists on the cluster.
 func (in *kialiCacheImpl) IsAmbientEnabled(cluster string) bool {
diff --git a/kubernetes/types.go b/kubernetes/types.go
index ebd0ac3ff..b8f0daeed 100644
--- a/kubernetes/types.go
+++ b/kubernetes/types.go
@@ -252,7 +252,8 @@ type IstioService struct {
 		Port     int    `json:"port"`
 		Protocol string `json:"protocol,omitempty"`
 	} `json:"ports"`
-	Hostname string `json:"hostname"`
+	Hostname        string `json:"hostname"`
+	ResourceVersion string `json:"ResourceVersion"`
 	// ClusterVIPs defined in Istio 1.11.x
 	ClusterVIPs11 map[string]string `json:"cluster-vips,omitempty"`
 	// ClusterVIPs defined in Istio 1.12.x
diff --git a/models/istio_validation.go b/models/istio_validation.go
index f72e24fbe..5467c0252 100644
--- a/models/istio_validation.go
+++ b/models/istio_validation.go
@@ -415,6 +415,17 @@ func (iv IstioValidations) FilterByKey(objectGVK schema.GroupVersionKind, name s
 	return fiv
 }
 
+func (iv IstioValidations) FilterByCluster(cluster string) IstioValidations {
+	fiv := IstioValidations{}
+	for k, v := range iv {
+		if v.Cluster == cluster {
+			fiv[k] = v
+		}
+	}
+
+	return fiv
+}
+
 // FilterByTypes takes an input as ObjectTypes, transforms to singular types and filters the validations
 func (iv IstioValidations) FilterByTypes(objectTypes []string) IstioValidations {
 	types := make(map[string]bool, len(objectTypes))
diff --git a/models/workload.go b/models/workload.go
index 4be65342c..971ff5ead 100644
--- a/models/workload.go
+++ b/models/workload.go
@@ -172,6 +172,13 @@ type WorkloadListItem struct {
 
 	// Names of the waypoint proxy workloads, if any
 	WaypointWorkloads []string `json:"waypointWorkloads"`
+
+	// ValidationKey is a pre-calculated key string: "cluster:namespace:name"
+	ValidationKey string
+
+	// ValidationVersion is a pre-calculated string representing the workload "version", basically
+	// the workload information that, if changed, requires re-validation.
+	ValidationVersion string
 }
 
 type WorkloadOverviews []*WorkloadListItem
@@ -278,8 +285,7 @@ type Workloads []*Workload
 
 type WorkloadEntries []*WorkloadEntry
 
-func (workload *WorkloadListItem) ParseWorkload(w *Workload) {
-	conf := config.Get()
+func (workload *WorkloadListItem) ParseWorkload(w *Workload, conf *config.Config) {
 	workload.Name = w.Name
 	workload.Namespace = w.Namespace
 	workload.WorkloadGVK = w.WorkloadGVK
@@ -717,9 +723,9 @@ func (workloads WorkloadOverviews) HasIstioSidecar() bool {
 	return false
 }
 
-func (wl WorkloadList) GetLabels() []labels.Set {
-	wLabels := make([]labels.Set, 0, len(wl.Workloads))
-	for _, w := range wl.Workloads {
+func GetLabels(workloads []*Workload) []labels.Set {
+	wLabels := make([]labels.Set, 0, len(workloads))
+	for _, w := range workloads {
 		wLabels = append(wLabels, labels.Set(w.Labels))
 	}
 	return wLabels



Test patch:
diff --git a/business/checkers/common/multi_match_selector_checker_test.go b/business/checkers/common/multi_match_selector_checker_test.go
index a7d1e7776..4201b27d7 100644
--- a/business/checkers/common/multi_match_selector_checker_test.go
+++ b/business/checkers/common/multi_match_selector_checker_test.go
@@ -27,7 +27,7 @@ func TestTwoSidecarsWithSelector(t *testing.T) {
 				"app": "details",
 			}, data.CreateSidecar("sidecar2", "bookinfo")),
 		},
-		workloadList(),
+		workloads(),
 	).Check()
 
 	assert.Empty(validations)
@@ -42,7 +42,7 @@ func TestTwoSidecarsWithoutSelector(t *testing.T) {
 			data.CreateSidecar("sidecar2", "bookinfo"),
 			data.CreateSidecar("sidecar3", "bookinfo2"),
 		},
-		workloadList(),
+		workloads(),
 	).Check()
 
 	assertMultimatchFailure(t, "generic.multimatch.selectorless", validations, "sidecar1", []string{"sidecar2"})
@@ -59,7 +59,7 @@ func TestTwoSidecarsWithoutSelectorDifferentNamespaces(t *testing.T) {
 			data.CreateSidecar("sidecar1", "bookinfo"),
 			data.CreateSidecar("sidecar2", "bookinfo2"),
 		},
-		workloadList(),
+		workloads(),
 	).Check()
 
 	assert.Empty(validations)
@@ -86,7 +86,7 @@ func TestTwoSidecarsTargetingOneDeployment(t *testing.T) {
 		config.Get().KubernetesConfig.ClusterName,
 		kubernetes.Sidecars,
 		sidecars,
-		workloadList(),
+		workloads(),
 	).Check()
 
 	assertMultimatchFailure(t, "generic.multimatch.selector", validations, "sidecar1", []string{"sidecar3", "sidecar4"})
@@ -117,7 +117,7 @@ func TestSidecarsCrossNamespaces(t *testing.T) {
 		config.Get().KubernetesConfig.ClusterName,
 		kubernetes.Sidecars,
 		sidecars,
-		workloadList(),
+		workloads(),
 	).Check()
 
 	assertMultimatchFailure(t, "generic.multimatch.selector", validations, "sidecar1", []string{"sidecar2"})
@@ -149,7 +149,7 @@ func TestSidecarsDifferentNamespaces(t *testing.T) {
 		config.Get().KubernetesConfig.ClusterName,
 		kubernetes.Sidecars,
 		sidecars,
-		workloadList(),
+		workloads(),
 	).Check()
 
 	assert.Empty(validations)
@@ -179,12 +179,12 @@ func assertMultimatchFailure(t *testing.T, code string, vals models.IstioValidat
 	}
 }
 
-func workloadList() map[string]models.WorkloadList {
-	wli := []models.WorkloadListItem{
-		data.CreateWorkloadListItem("details-v1", map[string]string{"app": "details", "version": "v1"}),
-		data.CreateWorkloadListItem("details-v2", map[string]string{"app": "details", "version": "v2"}),
-		data.CreateWorkloadListItem("details-v3", map[string]string{"app": "details", "version": "v3"}),
+func workloads() map[string]models.Workloads {
+	workloads := models.Workloads{
+		data.CreateWorkload("details-v1", map[string]string{"app": "details", "version": "v1"}),
+		data.CreateWorkload("details-v2", map[string]string{"app": "details", "version": "v2"}),
+		data.CreateWorkload("details-v3", map[string]string{"app": "details", "version": "v3"}),
 	}
 
-	return data.CreateWorkloadsPerNamespace([]string{"bookinfo", "bookinfo2", "bookinfo3", "bookinfo4"}, wli...)
+	return data.CreateWorkloadsPerNamespace([]string{"bookinfo", "bookinfo2", "bookinfo3", "bookinfo4"}, workloads)
 }
diff --git a/business/checkers/common/workload_selector_checker_test.go b/business/checkers/common/workload_selector_checker_test.go
index 7e0beec03..fbfb8143d 100644
--- a/business/checkers/common/workload_selector_checker_test.go
+++ b/business/checkers/common/workload_selector_checker_test.go
@@ -20,7 +20,7 @@ func TestPresentWorkloads(t *testing.T) {
 			"app":     "details",
 			"version": "v1",
 		},
-		workloadList(),
+		workloads(),
 	).Check()
 
 	// Well configured object
@@ -32,7 +32,7 @@ func TestPresentWorkloads(t *testing.T) {
 		map[string]string{
 			"app": "details",
 		},
-		workloadList(),
+		workloads(),
 	).Check()
 
 	// Well configured object
@@ -51,14 +51,14 @@ func TestWorkloadNotFound(t *testing.T) {
 }
 
 func testFailureWithWorkloadList(assert *assert.Assertions, selector map[string]string) {
-	testFailure(assert, selector, workloadList(), "generic.selector.workloadnotfound")
+	testFailure(assert, selector, workloads(), "generic.selector.workloadnotfound")
 }
 
 func testFailureWithEmptyWorkloadList(assert *assert.Assertions, selector map[string]string) {
-	testFailure(assert, selector, data.CreateWorkloadsPerNamespace([]string{"test"}, models.WorkloadListItem{}), "generic.selector.workloadnotfound")
+	testFailure(assert, selector, data.CreateWorkloadsPerNamespace([]string{"test"}, models.Workloads{}), "generic.selector.workloadnotfound")
 }
 
-func testFailure(assert *assert.Assertions, selector map[string]string, wl map[string]models.WorkloadList, code string) {
+func testFailure(assert *assert.Assertions, selector map[string]string, wl map[string]models.Workloads, code string) {
 	vals, valid := WorkloadSelectorNoWorkloadFoundChecker(
 		kubernetes.Sidecars,
 		selector,
diff --git a/business/checkers/destinationrules/no_dest_checker_test.go b/business/checkers/destinationrules/no_dest_checker_test.go
index 1c5169951..2a636c53b 100644
--- a/business/checkers/destinationrules/no_dest_checker_test.go
+++ b/business/checkers/destinationrules/no_dest_checker_test.go
@@ -26,10 +26,10 @@ func TestValidHost(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "reviews"),
@@ -43,10 +43,10 @@ func TestValidWildcardHost(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule: data.CreateTestDestinationRule("test-namespace",
@@ -64,10 +64,10 @@ func TestValidMeshWideHost(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "*.local"),
@@ -84,10 +84,10 @@ func TestValidShortSvcHost(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "reviews.test-namespace.svc"),
@@ -104,10 +104,10 @@ func TestValidServiceNamespace(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "reviews.test-namespace"),
@@ -128,10 +128,10 @@ func TestValidServiceNamespaceInvalid(t *testing.T) {
 			models.Namespace{Name: "test-namespace"},
 			models.Namespace{Name: "outside-ns"},
 		},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "reviews.not-a-namespace"),
@@ -155,10 +155,10 @@ func TestValidServiceNamespaceCrossNamespace(t *testing.T) {
 			models.Namespace{Name: "test-namespace"},
 			models.Namespace{Name: "outside-ns"},
 		},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"outside-ns": data.CreateWorkloadList("outside-ns",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"outside-ns": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		// using outside-ns namespace in host where the workloads are created. this should not fail
 		DestinationRule: data.CreateTestDestinationRule("test-namespace", "name", "reviews.outside-ns.svc.cluster.local"),
@@ -178,10 +178,10 @@ func TestNoValidHost(t *testing.T) {
 
 	// reviews is not part of services
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("detailsv1", appVersionLabel("details", "v1")),
-				data.CreateWorkloadListItem("otherv1", appVersionLabel("other", "v1"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("detailsv1", appVersionLabel("details", "v1")),
+				data.CreateWorkload("otherv1", appVersionLabel("other", "v1"))},
 		},
 		RegistryServices: []*kubernetes.RegistryService{{}},
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "reviews"),
@@ -207,10 +207,10 @@ func TestNoValidShortSvcHost(t *testing.T) {
 	// Not valid:
 	// reviews.test-namespace.svc.cluster
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("detailsv1", appVersionLabel("details", "v1")),
-				data.CreateWorkloadListItem("otherv1", appVersionLabel("other", "v1"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("detailsv1", appVersionLabel("details", "v1")),
+				data.CreateWorkload("otherv1", appVersionLabel("other", "v1"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "reviews.test-namespace.svc.cluster"),
@@ -231,9 +231,9 @@ func TestNoMatchingSubset(t *testing.T) {
 
 	// reviews does not have v2 in known services
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v1"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v1"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateTestDestinationRule("test-namespace", "name", "reviews"),
@@ -274,10 +274,10 @@ func TestNoMatchingSubsetWithMoreLabels(t *testing.T) {
 		data.AddSubsetToDestinationRule(s2, data.CreateEmptyDestinationRule("test-namespace", "name", "reviews")))
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  dr,
@@ -308,10 +308,10 @@ func TestSubsetNotReferenced(t *testing.T) {
 
 	vals, valid := NoDestinationChecker{
 		Namespaces: models.Namespaces{models.Namespace{Name: "bookinfo2"}, models.Namespace{Name: "bookinfo"}},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"bookinfo": data.CreateWorkloadList("bookinfo",
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"bookinfo": {
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  dr,
@@ -340,10 +340,10 @@ func TestSubsetReferenced(t *testing.T) {
 
 	vals, valid := NoDestinationChecker{
 		Namespaces: models.Namespaces{models.Namespace{Name: "bookinfo2"}, models.Namespace{Name: "bookinfo"}},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"bookinfo": data.CreateWorkloadList("bookinfo",
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"bookinfo": {
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  dr,
@@ -376,10 +376,10 @@ func TestSubsetPresentMatchingNotReferenced(t *testing.T) {
 
 	vals, valid := NoDestinationChecker{
 		Namespaces: models.Namespaces{models.Namespace{Name: "bookinfo"}},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"bookinfo": data.CreateWorkloadList("bookinfo",
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"bookinfo": {
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "bookinfo"),
 		DestinationRule:  dr,
@@ -405,10 +405,10 @@ func TestWronglyReferenced(t *testing.T) {
 
 	vals, valid := NoDestinationChecker{
 		Namespaces: models.Namespaces{models.Namespace{Name: "bookinfo2"}, models.Namespace{Name: "bookinfo"}},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"bookinfo": data.CreateWorkloadList("bookinfo",
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviews", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"bookinfo": {
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviews", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  dr,
@@ -426,10 +426,10 @@ func TestFailCrossNamespaceHost(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		// Intentionally using the same serviceName, but different NS. This SHOULD fail to match the above workloads which are created in test-namespace
 		DestinationRule: data.CreateTestDestinationRule("test-namespace", "name", "reviews.different-ns.svc.cluster.local"),
@@ -644,10 +644,10 @@ func TestNoLabelsInSubset(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateNoLabelsDestinationRule("test-namespace", "name", "reviews"),
@@ -664,10 +664,10 @@ func TestSubsetWithoutLabels(t *testing.T) {
 	assert := assert.New(t)
 
 	vals, valid := NoDestinationChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2"))),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2"))},
 		},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 		DestinationRule:  data.CreateNoSubsetLabelsDestinationRule("test-namespace", "name", "reviews"),
diff --git a/business/checkers/gateways/selector_checker_test.go b/business/checkers/gateways/selector_checker_test.go
index 53e945df7..e2f94341a 100644
--- a/business/checkers/gateways/selector_checker_test.go
+++ b/business/checkers/gateways/selector_checker_test.go
@@ -22,10 +22,10 @@ func TestValidInternalSelector(t *testing.T) {
 
 	vals, valid := SelectorChecker{
 		Gateway: ingress,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("istio-system",
-				data.CreateWorkloadListItem("istio-ingressgateway", map[string]string{"istio": "ingressgateway"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("istio-ingressgateway", map[string]string{"istio": "ingressgateway"}),
+			}},
 		IsGatewayToNamespace: false,
 	}.Check()
 
@@ -34,10 +34,10 @@ func TestValidInternalSelector(t *testing.T) {
 
 	vals, valid = SelectorChecker{
 		Gateway: egress,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("istio-system",
-				data.CreateWorkloadListItem("istio-egressgateway", map[string]string{"istio": "egressgateway"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("istio-egressgateway", map[string]string{"istio": "egressgateway"}),
+			}},
 		IsGatewayToNamespace: false,
 	}.Check()
 
@@ -55,10 +55,10 @@ func TestValidNamespaceSelector(t *testing.T) {
 
 	vals, valid := SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("testproxy", map[string]string{"app": "proxy"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("testproxy", map[string]string{"app": "proxy"}),
+			}},
 		IsGatewayToNamespace: false,
 	}.Check()
 
@@ -77,10 +77,10 @@ func TestLocalNamespaceSelector(t *testing.T) {
 
 	vals, valid := SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("testproxy", map[string]string{"app": "proxy"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("testproxy", map[string]string{"app": "proxy"}),
+			}},
 		IsGatewayToNamespace: true,
 	}.Check()
 
@@ -92,10 +92,10 @@ func TestLocalNamespaceSelector(t *testing.T) {
 
 	vals, valid = SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("testproxy", map[string]string{"app": "proxy"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("testproxy", map[string]string{"app": "proxy"}),
+			}},
 		IsGatewayToNamespace: false,
 	}.Check()
 
@@ -107,10 +107,10 @@ func TestLocalNamespaceSelector(t *testing.T) {
 
 	vals, valid = SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("testproxy", map[string]string{"app": "proxy"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("testproxy", map[string]string{"app": "proxy"}),
+			}},
 		IsGatewayToNamespace: true,
 	}.Check()
 
@@ -122,10 +122,10 @@ func TestLocalNamespaceSelector(t *testing.T) {
 
 	vals, valid = SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("testproxy", map[string]string{"app": "proxy"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("testproxy", map[string]string{"app": "proxy"}),
+			}},
 		IsGatewayToNamespace: false,
 	}.Check()
 
@@ -143,10 +143,10 @@ func TestValidIstioNamespaceSelector(t *testing.T) {
 
 	vals, valid := SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"testproxy": data.CreateWorkloadList(conf.IstioNamespace,
-				data.CreateWorkloadListItem("testproxy", map[string]string{"app": "proxy"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"testproxy": {
+				data.CreateWorkload("testproxy", map[string]string{"app": "proxy"}),
+			}},
 		IsGatewayToNamespace: false,
 	}.Check()
 
@@ -155,11 +155,8 @@ func TestValidIstioNamespaceSelector(t *testing.T) {
 
 	vals, valid = SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": {
-				Namespace: "test",
-				Workloads: []models.WorkloadListItem{},
-			},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {},
 		},
 		IsGatewayToNamespace: false,
 	}.Check()
@@ -178,8 +175,8 @@ func TestMissingSelectorTarget(t *testing.T) {
 
 	vals, valid := SelectorChecker{
 		Gateway: gw,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test"),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {},
 		},
 		IsGatewayToNamespace: false,
 	}.Check()
diff --git a/business/checkers/no_service_checker_test.go b/business/checkers/no_service_checker_test.go
index fa38a13d8..caf6f279f 100644
--- a/business/checkers/no_service_checker_test.go
+++ b/business/checkers/no_service_checker_test.go
@@ -31,17 +31,17 @@ func TestAllIstioObjectWithServices(t *testing.T) {
 	assert := assert.New(t)
 
 	vals := NoServiceChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2")),
-				data.CreateWorkloadListItem("detailsv1", appVersionLabel("details", "v1")),
-				data.CreateWorkloadListItem("detailsv2", appVersionLabel("details", "v2")),
-				data.CreateWorkloadListItem("productv1", appVersionLabel("product", "v1")),
-				data.CreateWorkloadListItem("productv2", appVersionLabel("product", "v2")),
-				data.CreateWorkloadListItem("customerv1", appVersionLabel("customer", "v1")),
-				data.CreateWorkloadListItem("customerv2", appVersionLabel("customer", "v2"))),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2")),
+				data.CreateWorkload("detailsv1", appVersionLabel("details", "v1")),
+				data.CreateWorkload("detailsv2", appVersionLabel("details", "v2")),
+				data.CreateWorkload("productv1", appVersionLabel("product", "v1")),
+				data.CreateWorkload("productv2", appVersionLabel("product", "v2")),
+				data.CreateWorkload("customerv1", appVersionLabel("customer", "v1")),
+				data.CreateWorkload("customerv2", appVersionLabel("customer", "v2")),
+			}},
 		IstioConfigList:      fakeIstioConfigList(),
 		AuthorizationDetails: &kubernetes.RBACDetails{},
 		RegistryServices: append(data.CreateFakeRegistryServices("product.test.svc.cluster.local", "test", "test"),
@@ -63,15 +63,15 @@ func TestDetectObjectWithoutService(t *testing.T) {
 
 	vals := NoServiceChecker{
 		IstioConfigList: fakeIstioConfigList(),
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2")),
-				data.CreateWorkloadListItem("detailsv1", appVersionLabel("details", "v1")),
-				data.CreateWorkloadListItem("detailsv2", appVersionLabel("details", "v2")),
-				data.CreateWorkloadListItem("productv1", appVersionLabel("product", "v1")),
-				data.CreateWorkloadListItem("productv2", appVersionLabel("product", "v2"))),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2")),
+				data.CreateWorkload("detailsv1", appVersionLabel("details", "v1")),
+				data.CreateWorkload("detailsv2", appVersionLabel("details", "v2")),
+				data.CreateWorkload("productv1", appVersionLabel("product", "v1")),
+				data.CreateWorkload("productv2", appVersionLabel("product", "v2")),
+			}},
 		AuthorizationDetails: &kubernetes.RBACDetails{},
 		RegistryServices: append(data.CreateFakeRegistryServices("product.test.svc.cluster.local", "test", "."),
 			data.CreateFakeMultiRegistryServices([]string{"reviews.test.svc.cluster.local", "details.test.svc.cluster.local"}, "test", "*")...),
@@ -86,15 +86,15 @@ func TestDetectObjectWithoutService(t *testing.T) {
 	assert.NoError(validations.ConfirmIstioCheckMessage("destinationrules.nodest.matchingregistry", customerDr.Checks[0]))
 
 	vals = NoServiceChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2")),
-				data.CreateWorkloadListItem("detailsv1", appVersionLabel("details", "v1")),
-				data.CreateWorkloadListItem("detailsv2", appVersionLabel("details", "v2")),
-				data.CreateWorkloadListItem("customerv1", appVersionLabel("customer", "v1")),
-				data.CreateWorkloadListItem("customerv2", appVersionLabel("customer", "v2"))),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2")),
+				data.CreateWorkload("detailsv1", appVersionLabel("details", "v1")),
+				data.CreateWorkload("detailsv2", appVersionLabel("details", "v2")),
+				data.CreateWorkload("customerv1", appVersionLabel("customer", "v1")),
+				data.CreateWorkload("customerv2", appVersionLabel("customer", "v2")),
+			}},
 		IstioConfigList:      fakeIstioConfigList(),
 		RegistryServices:     data.CreateFakeMultiRegistryServices([]string{"reviews.test.svc.cluster.local", "details.test.svc.cluster.local", "customer.test.svc.cluster.local"}, "test", "*"),
 		AuthorizationDetails: &kubernetes.RBACDetails{},
@@ -111,15 +111,15 @@ func TestDetectObjectWithoutService(t *testing.T) {
 	assert.NoError(validations.ConfirmIstioCheckMessage("virtualservices.nohost.hostnotfound", productVs.Checks[1]))
 
 	vals = NoServiceChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2")),
-				data.CreateWorkloadListItem("productv1", appVersionLabel("product", "v1")),
-				data.CreateWorkloadListItem("productv2", appVersionLabel("product", "v2")),
-				data.CreateWorkloadListItem("customerv1", appVersionLabel("customer", "v1")),
-				data.CreateWorkloadListItem("customerv2", appVersionLabel("customer", "v2"))),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2")),
+				data.CreateWorkload("productv1", appVersionLabel("product", "v1")),
+				data.CreateWorkload("productv2", appVersionLabel("product", "v2")),
+				data.CreateWorkload("customerv1", appVersionLabel("customer", "v1")),
+				data.CreateWorkload("customerv2", appVersionLabel("customer", "v2")),
+			}},
 		IstioConfigList:      fakeIstioConfigList(),
 		RegistryServices:     data.CreateFakeMultiRegistryServices([]string{"reviews.test.svc.cluster.local", "product.test.svc.cluster.local", "customer.test.svc.cluster.local"}, "test", "*"),
 		AuthorizationDetails: &kubernetes.RBACDetails{},
@@ -129,15 +129,15 @@ func TestDetectObjectWithoutService(t *testing.T) {
 	assert.True(vals[models.IstioValidationKey{ObjectGVK: kubernetes.DestinationRules, Namespace: "test", Name: "customer-dr"}].Valid)
 
 	vals = NoServiceChecker{
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("productv1", appVersionLabel("product", "v1")),
-				data.CreateWorkloadListItem("productv2", appVersionLabel("product", "v2")),
-				data.CreateWorkloadListItem("detailsv1", appVersionLabel("details", "v1")),
-				data.CreateWorkloadListItem("detailsv2", appVersionLabel("details", "v2")),
-				data.CreateWorkloadListItem("customerv1", appVersionLabel("customer", "v1")),
-				data.CreateWorkloadListItem("customerv2", appVersionLabel("customer", "v2"))),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test": {
+				data.CreateWorkload("productv1", appVersionLabel("product", "v1")),
+				data.CreateWorkload("productv2", appVersionLabel("product", "v2")),
+				data.CreateWorkload("detailsv1", appVersionLabel("details", "v1")),
+				data.CreateWorkload("detailsv2", appVersionLabel("details", "v2")),
+				data.CreateWorkload("customerv1", appVersionLabel("customer", "v1")),
+				data.CreateWorkload("customerv2", appVersionLabel("customer", "v2")),
+			}},
 		IstioConfigList:      fakeIstioConfigList(),
 		RegistryServices:     data.CreateFakeMultiRegistryServices([]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "customer.test.svc.cluster.local"}, "test", "*"),
 		AuthorizationDetails: &kubernetes.RBACDetails{},
diff --git a/business/checkers/workloads/uncovered_workload_checker_test.go b/business/checkers/workloads/uncovered_workload_checker_test.go
index 210d38eea..8a07e42c4 100644
--- a/business/checkers/workloads/uncovered_workload_checker_test.go
+++ b/business/checkers/workloads/uncovered_workload_checker_test.go
@@ -25,7 +25,7 @@ func TestCoveredworkloads(t *testing.T) {
 	var vals []*models.IstioCheck
 	var valid bool
 
-	for _, wl := range workloadListNS2().Workloads {
+	for _, wl := range workloadsNS2() {
 		//firstCase - one authorization policy namespace is wide mesh and has no selector (covers all workloads including current workload),
 		// while other auths has diffrenet namespaces than current workload
 		vals, valid = UncoveredWorkloadChecker{
@@ -51,8 +51,9 @@ func TestCoveredworkloads(t *testing.T) {
 	}
 
 	// third case - curr workload in the same namespace and has a matching authorization policy (same ns and labels)
+	workloadsNS1 := workloadsNS1()
 	vals, valid = UncoveredWorkloadChecker{
-		Workload:              workloadListNS1().Workloads[1],
+		Workload:              workloadsNS1[1],
 		Namespace:             ns1,
 		AuthorizationPolicies: authorizationPoliciesNS1(),
 	}.Check()
@@ -65,25 +66,28 @@ func TestUnCoveredWorkloads(t *testing.T) {
 	conf := config.NewConfig()
 	config.Set(conf)
 
+	workloadsNS1 := workloadsNS1()
+	workloadsNS2 := workloadsNS2()
+
 	assert := assert.New(t)
 	//case 1 - authpolicy in root ns with unmatching selector, other auths from another ns
 	testFailure(assert, ns2,
 		variedAuthPolicies3(),
-		workloadListNS2().Workloads[0])
+		workloadsNS2[0])
 
 	// case 2 - curr workload has different namespace than of all authpolicies
-	testFailure(assert, ns2, authorizationPoliciesNS1(), workloadListNS2().Workloads[1])
+	testFailure(assert, ns2, authorizationPoliciesNS1(), workloadsNS2[1])
 
 	//case3 - workload and authpolicies have same namespace but all auth policies have unmatching labels
 	testFailure(assert, ns1,
 		authorizationPoliciesNS1(),
-		workloadListNS1().Workloads[3])
+		workloadsNS1[3])
 
 	//case4 - no authorization policy found
-	testFailure(assert, ns2, []*security_v1.AuthorizationPolicy{}, workloadListNS2().Workloads[0])
+	testFailure(assert, ns2, []*security_v1.AuthorizationPolicy{}, workloadsNS2[0])
 }
 
-func testFailure(assert *assert.Assertions, ns string, authpolicies []*security_v1.AuthorizationPolicy, workload models.WorkloadListItem) {
+func testFailure(assert *assert.Assertions, ns string, authpolicies []*security_v1.AuthorizationPolicy, workload *models.Workload) {
 	vals, valid := UncoveredWorkloadChecker{
 		Workload:              workload,
 		Namespace:             ns,
@@ -96,23 +100,23 @@ func testFailure(assert *assert.Assertions, ns string, authpolicies []*security_
 
 }
 
-func workloadListNS1() models.WorkloadList {
-	wlitems := []models.WorkloadListItem{
-		data.CreateWorkloadListItem("covered-workload1", map[string]string{"app": "ratings", "version": "v1"}),
-		data.CreateWorkloadListItem("covered-workload2", map[string]string{"app": "productpage", "version": "v1"}),
-		data.CreateWorkloadListItem("covered-workload3", map[string]string{"app": "details", "version": "v3"}),
-		data.CreateWorkloadListItem("uncovered-workload", map[string]string{"app": "wrong", "version": "v5"}),
+func workloadsNS1() models.Workloads {
+	workloads := models.Workloads{
+		data.CreateWorkload("covered-workload1", map[string]string{"app": "ratings", "version": "v1"}),
+		data.CreateWorkload("covered-workload2", map[string]string{"app": "productpage", "version": "v1"}),
+		data.CreateWorkload("covered-workload3", map[string]string{"app": "details", "version": "v3"}),
+		data.CreateWorkload("uncovered-workload", map[string]string{"app": "wrong", "version": "v5"}),
 	}
 
-	return data.CreateWorkloadList(ns1, wlitems...)
+	return workloads
 }
 
-func workloadListNS2() models.WorkloadList {
-	wlitems := []models.WorkloadListItem{
-		data.CreateWorkloadListItem("uncovered-workload1", map[string]string{"app": "ratings", "version": "v1"}),
-		data.CreateWorkloadListItem("uncovered-workload2", map[string]string{"app": "details", "version": "v2"}),
+func workloadsNS2() models.Workloads {
+	workloads := models.Workloads{
+		data.CreateWorkload("uncovered-workload1", map[string]string{"app": "ratings", "version": "v1"}),
+		data.CreateWorkload("uncovered-workload2", map[string]string{"app": "details", "version": "v2"}),
 	}
-	return data.CreateWorkloadList(ns2, wlitems...)
+	return workloads
 }
 
 func authorizationPoliciesNS1() []*security_v1.AuthorizationPolicy {
diff --git a/business/istio_validations_perf_test.go b/business/istio_validations_perf_test.go
index c330191ff..a78d57222 100644
--- a/business/istio_validations_perf_test.go
+++ b/business/istio_validations_perf_test.go
@@ -68,11 +68,12 @@ func TestGetValidationsPerf(t *testing.T) {
 		[]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "product2.test.svc.cluster.local", "customer.test.svc.cluster.local"})
 
 	now := time.Now()
-	vInfo, err := vs.NewValidationInfo(context.TODO(), []string{conf.KubernetesConfig.ClusterName})
+	vInfo, err := vs.NewValidationInfo(context.TODO(), []string{conf.KubernetesConfig.ClusterName}, nil)
 	require.NoError(err)
-	validations, err := vs.Validate(context.TODO(), conf.KubernetesConfig.ClusterName, vInfo)
+	validationPerformed, validations, err := vs.Validate(context.TODO(), conf.KubernetesConfig.ClusterName, vInfo)
 	require.NoError(err)
 	log.Debugf("Validation Performance test took %f seconds for %d namespaces", time.Since(now).Seconds(), numNs)
+	assert.True(validationPerformed)
 	assert.NotEmpty(validations)
 }
 
@@ -170,14 +171,19 @@ func BenchmarkValidate(b *testing.B) {
 	layer := NewWithBackends(k8sclients, k8sclients, nil, nil)
 	vs := NewValidationsService(&layer.IstioConfig, cache, &mesh, &namespace, &layer.Svc, k8sclients, &layer.Workload)
 
-	vInfo, err := vs.NewValidationInfo(context.TODO(), []string{conf.KubernetesConfig.ClusterName})
+	var changeMap ValidationChangeMap
+	if conf.ExternalServices.Istio.ValidationChangeDetectionEnabled {
+		changeMap = ValidationChangeMap{}
+	}
+
+	vInfo, err := vs.NewValidationInfo(context.TODO(), []string{conf.KubernetesConfig.ClusterName}, changeMap)
 	if err != nil {
 		b.Fatal(err)
 	}
 
 	b.ResetTimer()
 	for n := 0; n < b.N; n++ {
-		_, err = vs.Validate(context.TODO(), conf.KubernetesConfig.ClusterName, vInfo)
+		_, _, err = vs.Validate(context.TODO(), conf.KubernetesConfig.ClusterName, vInfo)
 		if err != nil {
 			b.Fatal(err)
 		}
diff --git a/business/istio_validations_test.go b/business/istio_validations_test.go
index b15b64a7d..bec16c8f9 100644
--- a/business/istio_validations_test.go
+++ b/business/istio_validations_test.go
@@ -62,16 +62,36 @@ func TestGetNamespaceValidations(t *testing.T) {
 	vs := mockCombinedValidationService(t, conf, fakeIstioConfigList(),
 		[]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "product2.test.svc.cluster.local", "customer.test.svc.cluster.local"})
 
-	vInfo, err := vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName})
+	var changeMap = map[string]string{}
+	vInfo, err := vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName}, changeMap)
 	require.NoError(err)
-	validations, err := vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
+	validationPerformed, validations, err := vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
 	require.NoError(err)
+	assert.True(validationPerformed)
 	vs.kialiCache.Validations().Replace(validations)
 
 	validations, err = vs.GetValidations(context.TODO(), conf.KubernetesConfig.ClusterName)
 	require.NoError(err)
 	require.NotEmpty(validations)
 	assert.True(validations[models.IstioValidationKey{ObjectGVK: kubernetes.VirtualServices, Namespace: "test", Name: "product-vs"}].Valid)
+
+	// simulate a reconcile w/o a config change should skip running the checkers (new vInfo but re-use the changemap)
+	vInfo, err = vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName}, changeMap)
+	require.NoError(err)
+	validationPerformed, validations, err = vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
+	require.NoError(err)
+	assert.False(validationPerformed)
+	assert.Nil(validations)
+
+	// refresh the config but keep the changeMap, and we should see new validations. (note PeerAuthentication config updates its ResourceVersion)
+	vs = mockCombinedValidationService(t, conf, fakeIstioConfigList(),
+		[]string{"details.test.svc.cluster.local", "product.test.svc.cluster.local", "product2.test.svc.cluster.local", "customer.test.svc.cluster.local"})
+	vInfo, err = vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName}, changeMap)
+	require.NoError(err)
+	validationPerformed, validations, err = vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
+	require.NoError(err)
+	assert.True(validationPerformed)
+	assert.NotNil(validations)
 }
 
 func TestGetIstioObjectValidations(t *testing.T) {
@@ -704,10 +724,11 @@ func TestValidatingSingleObjectUpdatesList(t *testing.T) {
 	v, err := vs.userClients[conf.KubernetesConfig.ClusterName].Istio().NetworkingV1().VirtualServices("test").Get(context.Background(), "product-vs", v1.GetOptions{})
 	require.NoError(err)
 
-	vInfo, err := vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName})
+	vInfo, err := vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName}, nil)
 	require.NoError(err)
-	validations, err := vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
+	validationPerformed, validations, err := vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
 	require.NoError(err)
+	assert.True(validationPerformed)
 	vs.kialiCache.Validations().Replace(validations)
 
 	currentValidations, err := vs.GetValidations(context.Background(), conf.KubernetesConfig.ClusterName)
@@ -721,10 +742,11 @@ func TestValidatingSingleObjectUpdatesList(t *testing.T) {
 	require.NoError(err)
 
 	// make sure validations are updated in a cache before retrieving them
-	vInfo, err = vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName})
+	vInfo, err = vs.NewValidationInfo(context.Background(), []string{conf.KubernetesConfig.ClusterName}, nil)
 	require.NoError(err)
-	validations, err = vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
+	validationPerformed, validations, err = vs.Validate(context.Background(), conf.KubernetesConfig.ClusterName, vInfo)
 	require.NoError(err)
+	assert.True(validationPerformed)
 	vs.kialiCache.Validations().Replace(validations)
 
 	updatedValidations, _, err := vs.ValidateIstioObject(context.Background(), conf.KubernetesConfig.ClusterName, "test", kubernetes.VirtualServices, "product-vs")
diff --git a/business/references/auth_policy_references_test.go b/business/references/auth_policy_references_test.go
index 543555d90..2165b1f10 100644
--- a/business/references/auth_policy_references_test.go
+++ b/business/references/auth_policy_references_test.go
@@ -24,11 +24,10 @@ func prepareTestForAuthPolicy(ap *security_v1.AuthorizationPolicy, vs *networkin
 		AuthorizationPolicies: []*security_v1.AuthorizationPolicy{ap},
 		ServiceEntries:        []*networking_v1.ServiceEntry{se},
 		VirtualServices:       []*networking_v1.VirtualService{vs},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"istio-system": data.CreateWorkloadList("istio-system",
-				data.CreateWorkloadListItem("istiod", map[string]string{"app": "istio-ingressgateway"}),
-			),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"istio-system": {
+				data.CreateWorkload("istiod", map[string]string{"app": "istio-ingressgateway"}),
+			}},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("foo-dev", "istio-system"),
 	}
 	return *drReferences.References()[models.IstioReferenceKey{ObjectGVK: kubernetes.AuthorizationPolicies, Namespace: ap.Namespace, Name: ap.Name}]
diff --git a/business/references/destination_rule_references_test.go b/business/references/destination_rule_references_test.go
index 888852894..e8dc5a194 100644
--- a/business/references/destination_rule_references_test.go
+++ b/business/references/destination_rule_references_test.go
@@ -22,13 +22,13 @@ func prepareTestForDestinationRule(dr *networking_v1.DestinationRule, vs *networ
 		},
 		DestinationRules: []*networking_v1.DestinationRule{dr},
 		VirtualServices:  []*networking_v1.VirtualService{vs},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test-namespace": data.CreateWorkloadList("test-namespace",
-				data.CreateWorkloadListItem("reviewsv1", appVersionLabel("reviews", "v1")),
-				data.CreateWorkloadListItem("reviewsv2", appVersionLabel("reviews", "v2")),
-				data.CreateWorkloadListItem("reviewsv3", appVersionLabel("reviews", "v3")),
-				data.CreateWorkloadListItem("reviewsv4", appVersionLabel("reviews", "v4"))),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"test-namespace": {
+				data.CreateWorkload("reviewsv1", appVersionLabel("reviews", "v1")),
+				data.CreateWorkload("reviewsv2", appVersionLabel("reviews", "v2")),
+				data.CreateWorkload("reviewsv3", appVersionLabel("reviews", "v3")),
+				data.CreateWorkload("reviewsv4", appVersionLabel("reviews", "v4")),
+			}},
 		ServiceEntries:   []*networking_v1.ServiceEntry{fakeServiceEntry()},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("reviews", "test-namespace"),
 	}
diff --git a/business/references/gateway_references_test.go b/business/references/gateway_references_test.go
index d528499b3..53b5ef5c3 100644
--- a/business/references/gateway_references_test.go
+++ b/business/references/gateway_references_test.go
@@ -16,10 +16,10 @@ func prepareTestForGateway(gw *networking_v1.Gateway, vss []*networking_v1.Virtu
 	gwReferences := GatewayReferences{
 		Gateways:        []*networking_v1.Gateway{gw},
 		VirtualServices: vss,
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"test": data.CreateWorkloadList("istio-system",
-				data.CreateWorkloadListItem("istio-ingressgateway", map[string]string{"istio": "ingressgateway"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"istio-system": {
+				data.CreateWorkload("istio-ingressgateway", map[string]string{"istio": "ingressgateway"}),
+			}},
 	}
 	return *gwReferences.References()[models.IstioReferenceKey{ObjectGVK: kubernetes.Gateways, Namespace: gw.Namespace, Name: gw.Name}]
 }
diff --git a/business/references/k8s_gateway_references_test.go b/business/references/k8s_gateway_references_test.go
index c239f570e..64dc43706 100644
--- a/business/references/k8s_gateway_references_test.go
+++ b/business/references/k8s_gateway_references_test.go
@@ -28,12 +28,13 @@ func TestK8sGatewayReferences(t *testing.T) {
 		K8sGateways:   []*k8s_networking_v1.Gateway{gw},
 		K8sHTTPRoutes: []*k8s_networking_v1.HTTPRoute{r1, r2},
 		K8sGRPCRoutes: []*k8s_networking_v1.GRPCRoute{r3, r4},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"bookinfo": data.CreateWorkloadList("bookinfo",
-				data.CreateWorkloadListItem("bookinfo", map[string]string{conf.IstioLabels.AmbientWaypointGatewayLabel: "bookinfo"})),
-			"test": data.CreateWorkloadList("test",
-				data.CreateWorkloadListItem("test", map[string]string{conf.IstioLabels.AmbientWaypointGatewayLabel: "bookinfo"})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"bookinfo": {
+				data.CreateWorkload("bookinfo", map[string]string{conf.IstioLabels.AmbientWaypointGatewayLabel: "bookinfo"}),
+			},
+			"test": {
+				data.CreateWorkload("test", map[string]string{conf.IstioLabels.AmbientWaypointGatewayLabel: "bookinfo"}),
+			}},
 	}
 
 	references := gatewayReferences.References()
diff --git a/business/references/peer_auth_references_test.go b/business/references/peer_auth_references_test.go
index a62037f21..010f79151 100644
--- a/business/references/peer_auth_references_test.go
+++ b/business/references/peer_auth_references_test.go
@@ -20,11 +20,13 @@ func prepareTestForPeerAuth(pa *security_v1.PeerAuthentication, drs []*networkin
 			DestinationRules:    drs,
 			EnabledAutoMtls:     false,
 		},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"istio-system": data.CreateWorkloadList("istio-system",
-				data.CreateWorkloadListItem("grafana", map[string]string{"app": "grafana"})),
-			"bookinfo": data.CreateWorkloadList("bookinfo",
-				data.CreateWorkloadListItem("details", map[string]string{"app": "details"})),
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"istio-system": {
+				data.CreateWorkload("grafana", map[string]string{"app": "grafana"}),
+			},
+			"bookinfo": {
+				data.CreateWorkload("details", map[string]string{"app": "details"}),
+			},
 		},
 	}
 	return *drReferences.References()[models.IstioReferenceKey{ObjectGVK: kubernetes.PeerAuthentications, Namespace: pa.Namespace, Name: pa.Name}]
diff --git a/business/references/sidecar_references_test.go b/business/references/sidecar_references_test.go
index 7fe8cfc61..409bec3a8 100644
--- a/business/references/sidecar_references_test.go
+++ b/business/references/sidecar_references_test.go
@@ -20,11 +20,10 @@ func prepareTestForSidecar(sc *networking_v1.Sidecar, vs *networking_v1.VirtualS
 		},
 		Sidecars:       []*networking_v1.Sidecar{sc},
 		ServiceEntries: []*networking_v1.ServiceEntry{se},
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"istio-system": data.CreateWorkloadList("istio-system",
-				data.CreateWorkloadListItem("istiod", map[string]string{"app": "istio-ingressgateway"}),
-			),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"istio-system": {
+				data.CreateWorkload("istiod", map[string]string{"app": "istio-ingressgateway"}),
+			}},
 		RegistryServices: data.CreateFakeRegistryServicesLabels("foo-service", "istio-system"),
 	}
 	return *drReferences.References()[models.IstioReferenceKey{ObjectGVK: kubernetes.Sidecars, Namespace: sc.Namespace, Name: sc.Name}]
diff --git a/business/references/workloadgroup_references_test.go b/business/references/workloadgroup_references_test.go
index 0616264b4..049e9a1aa 100644
--- a/business/references/workloadgroup_references_test.go
+++ b/business/references/workloadgroup_references_test.go
@@ -15,13 +15,13 @@ func prepareTestForWorkloadGroup(name string) models.IstioReferences {
 	wgReferences := WorkloadGroupReferences{
 		WorkloadGroups:  data.CreateWorkloadGroups(*config.Get()),
 		WorkloadEntries: data.CreateWorkloadEntries(*config.Get()),
-		WorkloadsPerNamespace: map[string]models.WorkloadList{
-			"Namespace": data.CreateWorkloadList("Namespace",
-				data.CreateWorkloadListItem("ratings-vm", map[string]string{"app": "ratings-vm", "class": "vm", "version": "v3"}),
-				data.CreateWorkloadListItem("ratings-vm2", map[string]string{"app": "ratings-vm2", "class": "vm2", "version": "v4"}),
-				data.CreateWorkloadListItem("ratings-vm-no-entry", map[string]string{"app": "ratings-vm-no-entry", "class": "vm3"}),
-				data.CreateWorkloadListItem("ratings-vm-no-labels", map[string]string{})),
-		},
+		WorkloadsPerNamespace: map[string]models.Workloads{
+			"Namespace": {
+				data.CreateWorkload("ratings-vm", map[string]string{"app": "ratings-vm", "class": "vm", "version": "v3"}),
+				data.CreateWorkload("ratings-vm2", map[string]string{"app": "ratings-vm2", "class": "vm2", "version": "v4"}),
+				data.CreateWorkload("ratings-vm-no-entry", map[string]string{"app": "ratings-vm-no-entry", "class": "vm3"}),
+				data.CreateWorkload("ratings-vm-no-labels", map[string]string{}),
+			}},
 	}
 	return *wgReferences.References()[models.IstioReferenceKey{ObjectGVK: kubernetes.WorkloadGroups, Namespace: "Namespace", Name: name}]
 }
diff --git a/mesh/api/testdata/test_mesh_graph.expected b/mesh/api/testdata/test_mesh_graph.expected
index 596abd954..bab5cad21 100644
--- a/mesh/api/testdata/test_mesh_graph.expected
+++ b/mesh/api/testdata/test_mesh_graph.expected
@@ -319,6 +319,7 @@
             "Registry": null,
             "RootNamespace": "istio-system",
             "UrlServiceVersion": "",
+            "ValidationChangeDetectionEnabled": true,
             "ValidationReconcileInterval": 60000000000
           }
         }
diff --git a/tests/data/policy_data.go b/tests/data/policy_data.go
index 37d9b8ec6..c5e082638 100644
--- a/tests/data/policy_data.go
+++ b/tests/data/policy_data.go
@@ -1,6 +1,9 @@
 package data
 
 import (
+	"fmt"
+	"math/rand/v2"
+
 	api_security_v1 "istio.io/api/security/v1"
 	api_security_v1beta1 "istio.io/api/security/v1beta1"
 	api_v1beta1 "istio.io/api/type/v1beta1"
@@ -11,6 +14,7 @@ func CreateEmptyPeerAuthentication(name, namespace string, mtls *api_security_v1
 	pa := security_v1.PeerAuthentication{}
 	pa.Name = name
 	pa.Namespace = namespace
+	pa.ResourceVersion = fmt.Sprintf("%d", rand.Int64())
 	pa.Spec.Mtls = mtls
 	return &pa
 }
diff --git a/tests/data/workload_data.go b/tests/data/workload_data.go
index 0de93446a..d4f8e3c81 100644
--- a/tests/data/workload_data.go
+++ b/tests/data/workload_data.go
@@ -12,10 +12,10 @@ import (
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-func CreateWorkloadsPerNamespace(namespaces []string, items ...models.WorkloadListItem) map[string]models.WorkloadList {
-	allWorkloads := map[string]models.WorkloadList{}
+func CreateWorkloadsPerNamespace(namespaces []string, workloads models.Workloads) map[string]models.Workloads {
+	allWorkloads := map[string]models.Workloads{}
 	for _, namespace := range namespaces {
-		allWorkloads[namespace] = CreateWorkloadList(namespace, items...)
+		allWorkloads[namespace] = workloads
 	}
 	return allWorkloads
 }
@@ -27,6 +27,22 @@ func CreateWorkloadList(namespace string, items ...models.WorkloadListItem) mode
 	}
 }
 
+func CreateWorkload(name string, labels map[string]string) *models.Workload {
+	w := models.Workload{}
+	w.Name = name
+	w.Labels = labels
+
+	if _, found := labels["app"]; found {
+		w.AppLabel = true
+	}
+
+	if _, found := labels["version"]; found {
+		w.VersionLabel = true
+	}
+
+	return &w
+}
+
 func CreateWorkloadListItem(name string, labels map[string]string) models.WorkloadListItem {
 	wli := models.WorkloadListItem{
 		Name:   name,