diff --git a/include/libcamera/internal/software_isp/swisp_stats.h b/include/libcamera/internal/software_isp/swisp_stats.h
index ae11f112..3c986018 100644
--- a/include/libcamera/internal/software_isp/swisp_stats.h
+++ b/include/libcamera/internal/software_isp/swisp_stats.h
@@ -20,6 +20,11 @@ namespace libcamera {
  * wrap around.
  */
 struct SwIspStats {
+	/**
+	 * \brief True if the statistics buffer contains valid data, false if
+	 *        no statistics were generated for this frame
+	 */
+	bool valid;
 	/**
 	 * \brief Holds the sum of all sampled red pixels
 	 */
diff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp
index 3471cc88..af660a03 100644
--- a/src/ipa/simple/algorithms/agc.cpp
+++ b/src/ipa/simple/algorithms/agc.cpp
@@ -91,6 +91,9 @@ void Agc::updateExposure(IPAContext &context, IPAFrameContext &frameContext, dou
 	again = std::clamp(again, context.configuration.agc.againMin,
 			   context.configuration.agc.againMax);
 
+	context.activeState.agc.exposure = exposure;
+	context.activeState.agc.again = again;
+
 	LOG(IPASoftExposure, Debug)
 		<< "exposureMSV " << exposureMSV
 		<< " exp " << exposure << " again " << again;
@@ -107,6 +110,16 @@ void Agc::process(IPAContext &context,
 	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
 	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
 
+	if (!stats->valid) {
+		/*
+		 * Use the new exposure and gain values calculated the last time
+		 * there were valid stats.
+		 */
+		frameContext.sensor.exposure = context.activeState.agc.exposure;
+		frameContext.sensor.gain = context.activeState.agc.again;
+		return;
+	}
+
 	/*
 	 * Calculate Mean Sample Value (MSV) according to formula from:
 	 * https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf
diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
index cf567e89..ddd0b791 100644
--- a/src/ipa/simple/algorithms/awb.cpp
+++ b/src/ipa/simple/algorithms/awb.cpp
@@ -61,6 +61,9 @@ void Awb::process(IPAContext &context,
 	};
 	metadata.set(controls::ColourGains, mdGains);
 
+	if (!stats->valid)
+		return;
+
 	/*
 	 * Black level must be subtracted to get the correct AWB ratios, they
 	 * would be off if they were computed from the whole brightness range
diff --git a/src/ipa/simple/algorithms/blc.cpp b/src/ipa/simple/algorithms/blc.cpp
index 8c1e9ed0..616da0ee 100644
--- a/src/ipa/simple/algorithms/blc.cpp
+++ b/src/ipa/simple/algorithms/blc.cpp
@@ -60,6 +60,9 @@ void BlackLevel::process(IPAContext &context,
 	};
 	metadata.set(controls::SensorBlackLevels, blackLevels);
 
+	if (!stats->valid)
+		return;
+
 	if (context.configuration.black.level.has_value())
 		return;
 
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 468fccab..441f2a73 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -37,6 +37,10 @@ struct IPASessionConfiguration {
 };
 
 struct IPAActiveState {
+	struct {
+		int32_t exposure;
+		double again;
+	} agc;
 	struct {
 		uint8_t level;
 		int32_t lastExposure;
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 2dc85e5e..bfa60888 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -851,7 +851,7 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 	 *
 	 * \todo Pass real bufferId once stats buffer passing is changed.
 	 */
-	stats_->finishFrame(frame, 0);
+	stats_->finishFrame(frame, 0, true);
 	outputBufferReady.emit(output);
 	inputBufferReady.emit(input);
 }
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index 4b77b360..da91f912 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -313,11 +313,13 @@ void SwStatsCpu::startFrame(void)
  * \brief Finish statistics calculation for the current frame
  * \param[in] frame The frame number
  * \param[in] bufferId ID of the statistics buffer
+ * \param[in] valid Indicates if the statistics for the current frame are valid
  *
  * 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, uint32_t bufferId, bool valid)
 {
+	stats_.valid = valid;
 	*sharedStats_ = stats_;
 	statsReady.emit(frame, bufferId);
 }
diff --git a/src/libcamera/software_isp/swstats_cpu.h b/src/libcamera/software_isp/swstats_cpu.h
index 26a2f462..6ac3c4de 100644
--- a/src/libcamera/software_isp/swstats_cpu.h
+++ b/src/libcamera/software_isp/swstats_cpu.h
@@ -41,7 +41,7 @@ public:
 	int configure(const StreamConfiguration &inputCfg);
 	void setWindow(const Rectangle &window);
 	void startFrame();
-	void finishFrame(uint32_t frame, uint32_t bufferId);
+	void finishFrame(uint32_t frame, uint32_t bufferId, bool valid);
 
 	void processLine0(unsigned int y, const uint8_t *src[])
 	{
