diff --git a/src/ipa/softisp/softisp.cpp b/src/ipa/softisp/softisp.cpp
index aec04c528..5a20475b1 100644
--- a/src/ipa/softisp/softisp.cpp
+++ b/src/ipa/softisp/softisp.cpp
@@ -228,10 +228,26 @@ int IPASoftIsp::configure(const IPAConfigInfo &configInfo)
 		context_.configuration.agc.againMin = camHelper_->gain(againMin);
 		context_.configuration.agc.againMax = camHelper_->gain(againMax);
 		context_.configuration.agc.again10 = std::max(context_.configuration.agc.againMin, 1.0);
+		/*
+		 * The smallest gain change the sensor can actually make.
+		 *
+		 * The AGC needs this to know when a multiplicative correction
+		 * is too small to reach the next gain code, so that it steps
+		 * by one code instead of asking for a change that quantises
+		 * away and stalls the loop.
+		 *
+		 * Taking a fixed fraction of the total range instead ties the
+		 * step to how much gain the sensor offers, which is unrelated
+		 * to how finely it can be set. On a sensor whose range runs
+		 * from 1 to 248 that fraction is 2.47, so the smallest
+		 * correction the AGC can make is 12% at a typical indoor gain,
+		 * far larger than the correction it wants. The loop then
+		 * cannot settle: every step crosses the target and the
+		 * exposure oscillates about it indefinitely.
+		 */
 		context_.configuration.agc.againMinStep =
-			(context_.configuration.agc.againMax -
-			 context_.configuration.agc.againMin) /
-			100.0;
+			camHelper_->gain(againMin + 1) -
+			camHelper_->gain(againMin);
 		if (camHelper_->blackLevel().has_value()) {
 			/*
 			 * The black level from camHelper_ is a 16 bit value, software ISP
