diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
index 451596163..24eeaeb86 100644
--- a/include/libcamera/internal/software_isp/software_isp.h
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -105,6 +105,7 @@ private:
 	Thread ispWorkerThread_;
 	std::map<uint32_t, SharedMemObject<DebayerParams>> sharedParams_;
 	std::vector<uint32_t> availableParams_;
+	std::vector<uint32_t> availableStats_;
 	DmaBufAllocator dmaHeap_;
 	bool ccmEnabled_;
 
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 09e7833ed..f43428432 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -550,7 +550,7 @@ int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const Debaye
 }
 
 void DebayerEGL::process(uint32_t frame,
-			 [[maybe_unused]] const uint32_t statsBufferId,
+			 const uint32_t statsBufferId,
 			 const uint32_t paramsBufferId,
 			 FrameBuffer *input,
 			 FrameBuffer *output)
@@ -590,7 +590,7 @@ void DebayerEGL::process(uint32_t frame,
 			inDmaSyncer.emplace(input->planes()[0].fd, DmaSyncer::SyncType::Read);
 			inMapped.emplace(input, MappedFrameBuffer::MapFlag::Read);
 		}
-		stats_->processFrame(frame, 0, inMapped.value());
+		stats_->processFrame(frame, statsBufferId, inMapped.value());
 	}
 	inDmaSyncer.reset();
 
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index c4da647e8..2a019e6bc 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -225,6 +225,7 @@ std::unique_ptr<SwStatsCpu> SoftwareIsp::allocateStatsBuffers(
 
 		fdStats[bufferId] = shared.fd();
 		sharedStats->emplace(bufferId, std::move(shared));
+		availableStats_.push_back(bufferId);
 	}
 
 	auto stats = std::make_unique<SwStatsCpu>(cm, std::move(sharedStats));
@@ -463,7 +464,8 @@ void SoftwareIsp::stop()
  * \param[in] frame The frame number
  * \param[in] input The input framebuffer
  * \param[out] output The framebuffer to write the processed frame to
- * \return 0 on success, -EAGAIN if a parameter buffer underrun occurs
+ * \return 0 on success, -EAGAIN if a parameter or statistics buffer underrun
+ *   occurs
  */
 int SoftwareIsp::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output)
 {
@@ -471,11 +473,16 @@ int SoftwareIsp::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 		LOG(SoftwareIsp, Error) << "Parameters buffer underrun";
 		return -EAGAIN;
 	}
+	if (availableStats_.empty()) {
+		LOG(SoftwareIsp, Error) << "Statistics buffer underrun";
+		return -EAGAIN;
+	}
 
 	const uint32_t paramsBufferId = availableParams_.back();
 	availableParams_.pop_back();
+	const uint32_t statsBufferId = availableStats_.back();
+	availableStats_.pop_back();
 	ipa_->computeParams(frame, paramsBufferId);
-	const uint32_t statsBufferId = 0;
 	debayer_->invokeMethod(&Debayer::process,
 			       ConnectionTypeQueued, frame,
 			       statsBufferId, paramsBufferId, input, output);
@@ -498,8 +505,9 @@ void SoftwareIsp::statsReady(uint32_t frame, const uint32_t statsBufferId)
 	ispStatsReady.emit(frame, statsBufferId);
 }
 
-void SoftwareIsp::statsProcessed([[maybe_unused]] const uint32_t statsBufferId)
+void SoftwareIsp::statsProcessed(const uint32_t statsBufferId)
 {
+	availableStats_.push_back(statsBufferId);
 }
 
 void SoftwareIsp::inputReady(FrameBuffer *input)
