diff --git a/src/ipa/raspberrypi/controller/rpi/agc.cpp b/src/ipa/raspberrypi/controller/rpi/agc.cpp
index eaabc5fa9cd0..ee81537c653c 100644
--- a/src/ipa/raspberrypi/controller/rpi/agc.cpp
+++ b/src/ipa/raspberrypi/controller/rpi/agc.cpp
@@ -309,14 +309,14 @@ void Agc::setFixedShutter(Duration fixedShutter)
 {
 	fixedShutter_ = fixedShutter;
 	/* Set this in case someone calls disableAuto() straight after. */
-	status_.shutterTime = clipShutter(fixedShutter_);
+	status_.shutterTime = limitShutter(fixedShutter_);
 }
 
 void Agc::setFixedAnalogueGain(double fixedAnalogueGain)
 {
 	fixedAnalogueGain_ = fixedAnalogueGain;
 	/* Set this in case someone calls disableAuto() straight after. */
-	status_.analogueGain = fixedAnalogueGain;
+	status_.analogueGain = limitGain(fixedAnalogueGain);
 }
 
 void Agc::setMeteringMode(std::string const &meteringModeName)
@@ -342,7 +342,7 @@ void Agc::switchMode(CameraMode const &cameraMode,
 
 	housekeepConfig();
 
-	Duration fixedShutter = clipShutter(fixedShutter_);
+	Duration fixedShutter = limitShutter(fixedShutter_);
 	if (fixedShutter && fixedAnalogueGain_) {
 		/* We're going to reset the algorithm here with these fixed values. */
 
@@ -516,7 +516,7 @@ void Agc::housekeepConfig()
 {
 	/* First fetch all the up-to-date settings, so no one else has to do it. */
 	status_.ev = ev_;
-	status_.fixedShutter = clipShutter(fixedShutter_);
+	status_.fixedShutter = limitShutter(fixedShutter_);
 	status_.fixedAnalogueGain = fixedAnalogueGain_;
 	status_.flickerPeriod = flickerPeriod_;
 	LOG(RPiAgc, Debug) << "ev " << status_.ev << " fixedShutter "
@@ -703,7 +703,7 @@ void Agc::computeTargetExposure(double gain)
 		Duration maxShutter = status_.fixedShutter
 					      ? status_.fixedShutter
 					      : exposureMode_->shutter.back();
-		maxShutter = clipShutter(maxShutter);
+		maxShutter = limitShutter(maxShutter);
 		Duration maxTotalExposure =
 			maxShutter *
 			(status_.fixedAnalogueGain != 0.0
@@ -803,15 +803,16 @@ void Agc::divideUpExposure()
 	double analogueGain;
 	shutterTime = status_.fixedShutter ? status_.fixedShutter
 					   : exposureMode_->shutter[0];
-	shutterTime = clipShutter(shutterTime);
+	shutterTime = limitShutter(shutterTime);
 	analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain
 							: exposureMode_->gain[0];
+	analogueGain = limitGain(analogueGain);
 	if (shutterTime * analogueGain < exposureValue) {
 		for (unsigned int stage = 1;
 		     stage < exposureMode_->gain.size(); stage++) {
 			if (!status_.fixedShutter) {
 				Duration stageShutter =
-					clipShutter(exposureMode_->shutter[stage]);
+					limitShutter(exposureMode_->shutter[stage]);
 				if (stageShutter * analogueGain >= exposureValue) {
 					shutterTime = exposureValue / analogueGain;
 					break;
@@ -824,6 +825,7 @@ void Agc::divideUpExposure()
 					break;
 				}
 				analogueGain = exposureMode_->gain[stage];
+				analogueGain = limitGain(analogueGain);
 			}
 		}
 	}
@@ -846,6 +848,7 @@ void Agc::divideUpExposure()
 			 * gain as a side-effect.
 			 */
 			analogueGain = std::min(analogueGain, exposureMode_->gain.back());
+			analogueGain = limitGain(analogueGain);
 			shutterTime = newShutterTime;
 		}
 		LOG(RPiAgc, Debug) << "After flicker avoidance, shutter "
@@ -872,13 +875,37 @@ void Agc::writeAndFinish(Metadata *imageMetadata, bool desaturate)
 			   << " analogue gain " << filtered_.analogueGain;
 }
 
-Duration Agc::clipShutter(Duration shutter)
+Duration Agc::limitShutter(Duration shutter)
 {
-	if (sensorLimits_.maxShutter)
-		shutter = std::min(shutter, sensorLimits_.maxShutter);
+	/*
+	 * shutter == 0 is a special case for fixed shutter values, and must pass
+	 * through unchanged
+	 */
+	if (!shutter)
+		return shutter;
+
+	shutter = std::clamp(shutter, sensorLimits_.minShutter,
+			     sensorLimits_.maxShutter);
 	return shutter;
 }
 
+double Agc::limitGain(double gain) const
+{
+	/*
+	 * Only limit the lower bounds of the gain value to what the sensor limits.
+	 * The upper bound on analogue gain will be made up with additional digital
+	 * gain applied by the ISP.
+	 *
+	 * gain == 0.0 is a special case for fixed shutter values, and must pass
+	 * through unchanged
+	 */
+	if (!gain)
+		return gain;
+
+	gain = std::max(gain, sensorLimits_.minAnalogueGain);
+	return gain;
+}
+
 /* Register algorithm with the system. */
 static Algorithm *create(Controller *controller)
 {
diff --git a/src/ipa/raspberrypi/controller/rpi/agc.h b/src/ipa/raspberrypi/controller/rpi/agc.h
index 150a5f6e4428..c48acbba61f0 100644
--- a/src/ipa/raspberrypi/controller/rpi/agc.h
+++ b/src/ipa/raspberrypi/controller/rpi/agc.h
@@ -103,7 +103,8 @@ private:
 	void filterExposure(bool desaturate);
 	void divideUpExposure();
 	void writeAndFinish(Metadata *imageMetadata, bool desaturate);
-	libcamera::utils::Duration clipShutter(libcamera::utils::Duration shutter);
+	libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);
+	double limitGain(double gain) const;
 	AgcMeteringMode *meteringMode_;
 	AgcExposureMode *exposureMode_;
 	AgcConstraintMode *constraintMode_;
