diff --git a/include/libcamera/internal/software_isp/swstats_cpu.h b/include/libcamera/internal/software_isp/swstats_cpu.h
index 551870921..cd5303a51 100644
--- a/include/libcamera/internal/software_isp/swstats_cpu.h
+++ b/include/libcamera/internal/software_isp/swstats_cpu.h
@@ -38,14 +38,6 @@ public:
 	SwStatsCpu(const CameraManager &cm);
 	~SwStatsCpu() = default;
 
-	/*
-	 * The combination of pipeline + sensor delays means that
-	 * exposure changes can take up to 3 frames to get applied,
-	 * Run stats once every 4 frames to ensure any previous
-	 * exposure changes have been applied.
-	 */
-	static constexpr uint32_t kStatPerNumFrames = 4;
-
 	bool isValid() const { return sharedStats_.fd().isValid(); }
 
 	const SharedFD &getStatsFD() { return sharedStats_.fd(); }
@@ -54,6 +46,7 @@ public:
 
 	int configure(const StreamConfiguration &inputCfg, unsigned int statsBufferCount = 1);
 	void setWindow(const Rectangle &window);
+	bool updating(uint32_t frame);
 	void startFrame(uint32_t frame);
 	void finishFrame(uint32_t frame, uint32_t bufferId);
 	void processFrame(uint32_t frame, uint32_t bufferId, MappedFrameBuffer &input);
@@ -88,6 +81,14 @@ private:
 	using statsProcessFn = void (SwStatsCpu::*)(const uint8_t *src[], SwIspStats &stats);
 	using processFrameFn = void (SwStatsCpu::*)(MappedFrameBuffer &in);
 
+	/*
+	 * The combination of pipeline + sensor delays means that
+	 * exposure changes can take up to 3 frames to get applied,
+	 * Run stats once every 4 frames to ensure any previous
+	 * exposure changes have been applied.
+	 */
+	static constexpr uint32_t kStatPerNumFrames = 4;
+
 	int setupStandardBayerOrder(BayerFormat::Order order);
 	/* Bayer 8 bpp unpacked */
 	void statsBGGR8Line0(const uint8_t *src[], SwIspStats &stats);
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 300822f9d..1a9dc1841 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -630,21 +630,17 @@ void DebayerEGL::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 	metadata.planes()[0].bytesused = output->planes()[0].length;
 
 	/* Calculate stats for the whole frame */
-	if (frame % SwStatsCpu::kStatPerNumFrames) {
-		stats_->finishFrame(frame, 0);
-	} else {
-		if (!inMapped) {
-			/*
-			 * The buffer was directly imported into EGL and thus
-			 * not mapped for texture upload. Do it now for the
-			 * CPU-based stats calculation.
-			 */
-			assert(!inDmaSyncer);
-			inDmaSyncer.emplace(input->planes()[0].fd, DmaSyncer::SyncType::Read);
-			inMapped.emplace(input, MappedFrameBuffer::MapFlag::Read);
-		}
-		stats_->processFrame(frame, 0, inMapped.value());
+	if (stats_->updating(frame) && !inMapped) {
+		/*
+		 * The buffer was directly imported into EGL and thus
+		 * not mapped for texture upload. Do it now for the
+		 * CPU-based stats calculation.
+		 */
+		assert(!inDmaSyncer);
+		inDmaSyncer.emplace(input->planes()[0].fd, DmaSyncer::SyncType::Read);
+		inMapped.emplace(input, MappedFrameBuffer::MapFlag::Read);
 	}
+	stats_->processFrame(frame, 0, inMapped.value());
 	inDmaSyncer.reset();
 
 	egl_.syncOutput();
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index 7fb77ce7d..362fb6a86 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -383,7 +383,7 @@ void SwStatsCpu::statsGBRG12PLine0(const uint8_t *src[], SwIspStats &stats)
  */
 void SwStatsCpu::startFrame(uint32_t frame)
 {
-	if (frame % kStatPerNumFrames)
+	if (!updating(frame))
 		return;
 
 	if (window_.width == 0)
@@ -404,7 +404,7 @@ void SwStatsCpu::startFrame(uint32_t frame)
  */
 void SwStatsCpu::finishFrame(uint32_t frame, uint32_t bufferId)
 {
-	bool valid = frame % kStatPerNumFrames == 0;
+	bool valid = updating(frame);
 
 	if (valid) {
 		sharedStats_->sum_ = RGB<uint64_t>({ 0, 0, 0 });
@@ -604,7 +604,7 @@ void SwStatsCpu::processBayerFrame2(MappedFrameBuffer &in)
  */
 void SwStatsCpu::processFrame(uint32_t frame, uint32_t bufferId, MappedFrameBuffer &input)
 {
-	if (frame % kStatPerNumFrames) {
+	if (!updating(frame)) {
 		finishFrame(frame, bufferId);
 		return;
 	}
@@ -616,4 +616,14 @@ void SwStatsCpu::processFrame(uint32_t frame, uint32_t bufferId, MappedFrameBuff
 	bench_.finishFrame();
 }
 
+/**
+ * \brief Determine whether stats update is required for the given frame
+ * \param[in] frame The frame number
+ * \return Whether the stats are updated for the given frame
+ */
+bool SwStatsCpu::updating(uint32_t frame)
+{
+	return frame % kStatPerNumFrames == 0;
+}
+
 } /* namespace libcamera */
