diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
index 9ea0ed4db..3e879c630 100644
--- a/include/libcamera/internal/software_isp/software_isp.h
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -92,7 +92,8 @@ private:
 	void paramsBufferReady(const uint32_t paramsBufferId);
 	bool allocateParamsBuffers(const unsigned int bufferCount);
 	void setSensorCtrls(const ControlList &sensorControls);
-	void statsReady(uint32_t frame, uint32_t bufferId);
+	void statsReady(uint32_t frame, const uint32_t statsBufferId);
+	void statsProcessed(const uint32_t statsBufferId);
 	void inputReady(FrameBuffer *input);
 	void outputReady(FrameBuffer *output);
 	std::unique_ptr<Debayer> debayer_;
diff --git a/include/libcamera/internal/software_isp/swstats_cpu.h b/include/libcamera/internal/software_isp/swstats_cpu.h
index 686e3d981..fd3f97cbb 100644
--- a/include/libcamera/internal/software_isp/swstats_cpu.h
+++ b/include/libcamera/internal/software_isp/swstats_cpu.h
@@ -55,8 +55,8 @@ public:
 	int configure(const StreamConfiguration &inputCfg, unsigned int statsBufferCount = 1);
 	void setWindow(const Rectangle &window);
 	void startFrame(uint32_t frame);
-	void finishFrame(uint32_t frame, uint32_t bufferId);
-	void processFrame(uint32_t frame, uint32_t bufferId, MappedFrameBuffer &input);
+	void finishFrame(uint32_t frame, uint32_t statsBufferId);
+	void processFrame(uint32_t frame, uint32_t statsBufferId, MappedFrameBuffer &input);
 
 	void processLine0(uint32_t frame, unsigned int y, const uint8_t *src[], unsigned int statsBufferIndex = 0)
 	{
diff --git a/include/libcamera/ipa/soft.mojom b/include/libcamera/ipa/soft.mojom
index f348f582d..c2c5fe382 100644
--- a/include/libcamera/ipa/soft.mojom
+++ b/include/libcamera/ipa/soft.mojom
@@ -27,12 +27,13 @@ interface IPASoftInterface {
 	[async] queueRequest(uint32 frame, libcamera.ControlList sensorControls);
 	[async] computeParams(uint32 frame, uint32 paramsBufferId);
 	[async] processStats(uint32 frame,
-			     uint32 bufferId,
+			     uint32 statsBufferId,
 			     libcamera.ControlList sensorControls);
 };
 
 interface IPASoftEventInterface {
 	setSensorControls(libcamera.ControlList sensorControls);
+	statsProcessed(uint32 statsBufferId);
 	paramsComputed(uint32 paramsBufferId);
 	metadataReady(uint32 frame, libcamera.ControlList metadata);
 };
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index 0212a3b52..ded75a970 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -298,7 +298,7 @@ void IPASoftSimple::computeParams(const uint32_t frame,
 }
 
 void IPASoftSimple::processStats(const uint32_t frame,
-				 [[maybe_unused]] const uint32_t bufferId,
+				 const uint32_t statsBufferId,
 				 const ControlList &sensorControls)
 {
 	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
@@ -312,6 +312,7 @@ void IPASoftSimple::processStats(const uint32_t frame,
 	for (const auto &algo : algorithms())
 		algo->process(context_, frame, frameContext, stats_, metadata);
 	metadataReady.emit(frame, metadata);
+	statsProcessed.emit(statsBufferId);
 
 	/* Sanity check */
 	if (!sensorControls.contains(V4L2_CID_EXPOSURE) ||
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index b49f372a6..2e6bff146 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -369,7 +369,7 @@ private:
 	void conversionInputDone(FrameBuffer *buffer);
 	void conversionOutputDone(FrameBuffer *buffer);
 
-	void ispStatsReady(uint32_t frame, uint32_t bufferId);
+	void ispStatsReady(uint32_t frame, const uint32_t statsBufferId);
 	void metadataReady(uint32_t frame, const ControlList &metadata);
 	void setSensorControls(const ControlList &sensorControls);
 };
@@ -1044,9 +1044,10 @@ void SimpleCameraData::conversionOutputDone(FrameBuffer *buffer)
 		tryCompleteRequest(request);
 }
 
-void SimpleCameraData::ispStatsReady(uint32_t frame, uint32_t bufferId)
+void SimpleCameraData::ispStatsReady(uint32_t frame,
+				     const uint32_t statsBufferId)
 {
-	swIsp_->processStats(frame, bufferId,
+	swIsp_->processStats(frame, statsBufferId,
 			     delayedCtrls_->get(frame));
 }
 
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index 28e04d0df..341930e5b 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -120,9 +120,10 @@ Debayer::~Debayer()
  */
 
 /**
- * \fn void Debayer::process(uint32_t frame, const uint32_t paramsBufferId, FrameBuffer *input, FrameBuffer *output)
+ * \fn void Debayer::process(uint32_t frame, const uint32_t statsBufferId, const uint32_t paramsBufferId, FrameBuffer *input, FrameBuffer *output)
  * \brief Process the bayer data into the requested format
  * \param[in] frame The frame number
+ * \param[in] statsBufferId The id of the stats buffer to use
  * \param[in] paramsBufferId The id of the params buffer in use
  * \param[in] input The input buffer
  * \param[in] output The output buffer
diff --git a/src/libcamera/software_isp/debayer.h b/src/libcamera/software_isp/debayer.h
index 6cefa4c01..259261b1a 100644
--- a/src/libcamera/software_isp/debayer.h
+++ b/src/libcamera/software_isp/debayer.h
@@ -49,6 +49,7 @@ public:
 	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) = 0;
 
 	virtual void process(uint32_t frame,
+			     const uint32_t statsBufferId,
 			     const uint32_t paramsBufferId,
 			     FrameBuffer *input, FrameBuffer *output) = 0;
 	virtual int start() { return 0; }
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 7813eb695..d00830960 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -974,8 +974,11 @@ void DebayerCpu::updateLookupTables(const DebayerParams *params)
 	params_ = *params;
 }
 
-void DebayerCpu::process(uint32_t frame, const uint32_t paramsBufferId,
-			 FrameBuffer *input, FrameBuffer *output)
+void DebayerCpu::process(uint32_t frame,
+			 const uint32_t statsBufferId,
+			 const uint32_t paramsBufferId,
+			 FrameBuffer *input,
+			 FrameBuffer *output)
 {
 	bench_.startFrame();
 
@@ -1026,12 +1029,7 @@ void DebayerCpu::process(uint32_t frame, const uint32_t paramsBufferId,
 	/* Measure before emitting signals */
 	bench_.finishFrame();
 
-	/*
-	 * Buffer ids are currently not used, so pass zeros as its parameter.
-	 *
-	 * \todo Pass real bufferId once stats buffer passing is changed.
-	 */
-	stats_->finishFrame(frame, 0);
+	stats_->finishFrame(frame, statsBufferId);
 	outputBufferReady.emit(output);
 	inputBufferReady.emit(input);
 }
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
index 56d9087da..37e45e9ea 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -45,8 +45,11 @@ public:
 	std::vector<PixelFormat> formats(PixelFormat input) override;
 	std::tuple<unsigned int, unsigned int>
 	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) override;
-	void process(uint32_t frame, const uint32_t paramsBufferId,
-		     FrameBuffer *input, FrameBuffer *output) override;
+	void process(uint32_t frame,
+		     const uint32_t statsBufferId,
+		     const uint32_t paramsBufferId,
+		     FrameBuffer *input,
+		     FrameBuffer *output) override;
 	int start() override;
 	void stop() override;
 	SizeRange sizes(PixelFormat inputFormat, const Size &inputSize) override;
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 77696f1e7..09e7833ed 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -549,8 +549,11 @@ int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const Debaye
 	return 0;
 }
 
-void DebayerEGL::process(uint32_t frame, const uint32_t paramsBufferId,
-			 FrameBuffer *input, FrameBuffer *output)
+void DebayerEGL::process(uint32_t frame,
+			 [[maybe_unused]] const uint32_t statsBufferId,
+			 const uint32_t paramsBufferId,
+			 FrameBuffer *input,
+			 FrameBuffer *output)
 {
 	bench_.startFrame();
 
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index 43e3dcb15..36c856c10 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -53,8 +53,11 @@ public:
 	std::vector<PixelFormat> formats(PixelFormat input) override;
 	std::tuple<unsigned int, unsigned int> strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) override;
 
-	void process(uint32_t frame, const uint32_t paramsBufferId,
-		     FrameBuffer *input, FrameBuffer *output) override;
+	void process(uint32_t frame,
+		     const uint32_t statsBufferId,
+		     const uint32_t paramsBufferId,
+		     FrameBuffer *input,
+		     FrameBuffer *output) override;
 	int start() override;
 	void stop() override;
 
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index dbe4cc270..c44e035b8 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -171,6 +171,7 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe,
 		return;
 	}
 
+	ipa_->statsProcessed.connect(this, &SoftwareIsp::statsProcessed);
 	ipa_->metadataReady.connect(this,
 				    [this](uint32_t frame, const ControlList &metadata) {
 					    metadataReady.emit(frame, metadata);
@@ -444,9 +445,10 @@ int SoftwareIsp::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 	const uint32_t paramsBufferId = availableParams_.back();
 	availableParams_.pop_back();
 	ipa_->computeParams(frame, paramsBufferId);
+	const uint32_t statsBufferId = 0;
 	debayer_->invokeMethod(&Debayer::process,
-			       ConnectionTypeQueued, frame, paramsBufferId,
-			       input, output);
+			       ConnectionTypeQueued, frame,
+			       statsBufferId, paramsBufferId, input, output);
 
 	return 0;
 }
@@ -461,9 +463,13 @@ void SoftwareIsp::setSensorCtrls(const ControlList &sensorControls)
 	setSensorControls.emit(sensorControls);
 }
 
-void SoftwareIsp::statsReady(uint32_t frame, uint32_t bufferId)
+void SoftwareIsp::statsReady(uint32_t frame, const uint32_t statsBufferId)
+{
+	ispStatsReady.emit(frame, statsBufferId);
+}
+
+void SoftwareIsp::statsProcessed([[maybe_unused]] const uint32_t statsBufferId)
 {
-	ispStatsReady.emit(frame, bufferId);
 }
 
 void SoftwareIsp::inputReady(FrameBuffer *input)
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index fb7a5301d..0e645f7e1 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -1,7 +1,7 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
  * Copyright (C) 2023, Linaro Ltd
- * Copyright (C) 2023, Red Hat Inc.
+ * Copyright (C) 2023-2026 Red Hat Inc.
  *
  * Authors:
  * Hans de Goede <hdegoede@redhat.com>
@@ -346,11 +346,12 @@ void SwStatsCpu::startFrame(uint32_t frame)
 /**
  * \brief Finish statistics calculation for the current frame
  * \param[in] frame The frame number
- * \param[in] bufferId ID of the statistics buffer
+ * \param[in] statsBufferId ID of the statistics buffer
  *
  * This may only be called after a successful setWindow() call.
  */
-void SwStatsCpu::finishFrame(uint32_t frame, uint32_t bufferId)
+void SwStatsCpu::finishFrame(uint32_t frame,
+			     const uint32_t statsBufferId)
 {
 	bool valid = frame % kStatPerNumFrames == 0;
 
@@ -367,7 +368,7 @@ void SwStatsCpu::finishFrame(uint32_t frame, uint32_t bufferId)
 	}
 
 	sharedStats_->valid = valid;
-	statsReady.emit(frame, bufferId);
+	statsReady.emit(frame, statsBufferId);
 }
 
 /**
@@ -538,22 +539,22 @@ void SwStatsCpu::processBayerFrame2(MappedFrameBuffer &in)
 /**
  * \brief Calculate statistics for a frame in one go
  * \param[in] frame The frame number
- * \param[in] bufferId ID of the statistics buffer
+ * \param[in] statsBufferId ID of the statistics buffer
  * \param[in] input The frame to process
  *
  * This may only be called after a successful setWindow() call.
  */
-void SwStatsCpu::processFrame(uint32_t frame, uint32_t bufferId, MappedFrameBuffer &input)
+void SwStatsCpu::processFrame(uint32_t frame, uint32_t statsBufferId, MappedFrameBuffer &input)
 {
 	if (frame % kStatPerNumFrames) {
-		finishFrame(frame, bufferId);
+		finishFrame(frame, statsBufferId);
 		return;
 	}
 
 	bench_.startFrame();
 	startFrame(frame);
 	(this->*processFrame_)(input);
-	finishFrame(frame, bufferId);
+	finishFrame(frame, statsBufferId);
 	bench_.finishFrame();
 }
 
