diff --git a/src/ipa/mali-c55/algorithms/agc.cpp b/src/ipa/mali-c55/algorithms/agc.cpp
index 6ecb18d959c7f978966d619beb43e1c2cbfeec5f..60224cc0e261709000901ab9a91c3566cbace6c5 100644
--- a/src/ipa/mali-c55/algorithms/agc.cpp
+++ b/src/ipa/mali-c55/algorithms/agc.cpp
@@ -170,21 +170,22 @@ int Agc::configure(IPAContext &context,
 	context.activeState.agc.exposureMode = exposureModeHelpers().begin()->first;
 
 	ControlInfo &frameDurationLimits = context.ctrlMap[&controls::FrameDurationLimits];
+	context.activeState.agc.minFrameDuration =
+		std::chrono::microseconds(frameDurationLimits.min().get<int64_t>());
+	context.activeState.agc.maxFrameDuration =
+		std::chrono::microseconds(frameDurationLimits.max().get<int64_t>());
 
 	AgcMeanLuminance::AgcSensorConfiguration sensorConfig;
 	sensorConfig.lineDuration = context.configuration.sensor.lineDuration;
 	sensorConfig.minExposureTime = context.configuration.agc.minShutterSpeed;
 	sensorConfig.maxExposureTime = context.configuration.agc.maxShutterSpeed;
-	sensorConfig.minFrameDuration =
-		std::chrono::microseconds(frameDurationLimits.min().get<int64_t>());
-	sensorConfig.maxFrameDuration =
-		std::chrono::microseconds(frameDurationLimits.max().get<int64_t>());
+	sensorConfig.minFrameDuration = context.activeState.agc.minFrameDuration;
+	sensorConfig.maxFrameDuration = context.activeState.agc.maxFrameDuration;
 	sensorConfig.minAnalogueGain = context.configuration.agc.minAnalogueGain;
 	sensorConfig.maxAnalogueGain = context.configuration.agc.maxAnalogueGain;
 
 	AgcMeanLuminance::configure(&sensorConfig, context.camHelper.get());
-
-	/* \todo Update AGC limits when FrameDurationLimits is passed in */
+	context.activeState.agc.maxFrameDuration = sensorConfig.maxFrameDuration;
 
 	resetFrameCount();
 
@@ -212,6 +213,25 @@ void Agc::queueRequest(IPAContext &context, const uint32_t frame,
 			<< " AGC";
 	}
 
+	const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);
+	if (frameDurationLimits) {
+		/* Limit the control value to the limits in ControlInfo */
+		ControlInfo &limits = context.ctrlMap[&controls::FrameDurationLimits];
+		int64_t minFrameDuration =
+			std::clamp((*frameDurationLimits).front(),
+				   limits.min().get<int64_t>(),
+				   limits.max().get<int64_t>());
+		int64_t maxFrameDuration =
+			std::clamp((*frameDurationLimits).back(),
+				   limits.min().get<int64_t>(),
+				   limits.max().get<int64_t>());
+
+		agc.minFrameDuration = std::chrono::microseconds(minFrameDuration);
+		agc.maxFrameDuration = std::chrono::microseconds(maxFrameDuration);
+	}
+	frameContext.agc.minFrameDuration = agc.minFrameDuration;
+	frameContext.agc.maxFrameDuration = agc.maxFrameDuration;
+
 	/*
 	 * If the automatic exposure and gain is enabled we have no further work
 	 * to do here...
@@ -375,6 +395,21 @@ void Agc::process(IPAContext &context,
 		return;
 	}
 
+	/*
+	 * Update the AGC limits using the frame duration.
+	 *
+	 * \todo Handle ExposureTime and AnalogueGain controls to support
+	 * manual mode.
+	 */
+	utils::Duration minExposureTime = context.configuration.agc.minShutterSpeed;
+	utils::Duration maxExposureTime = context.configuration.agc.maxShutterSpeed;
+	utils::Duration maxFrameDuration = frameContext.agc.maxFrameDuration;
+
+	setLimits(minExposureTime, maxExposureTime, maxFrameDuration,
+		  context.configuration.agc.minAnalogueGain,
+		  context.configuration.agc.maxAnalogueGain,
+		  {});
+
 	statistics_.parseStatistics(stats);
 	context.activeState.agc.temperatureK = estimateCCT({ { statistics_.rHist.interQuantileMean(0, 1),
 							       statistics_.gHist.interQuantileMean(0, 1),
@@ -402,13 +437,23 @@ void Agc::process(IPAContext &context,
 	dGain = std::clamp(dGain, kMinDigitalGain, kMaxDigitalGain);
 
 	LOG(MaliC55Agc, Debug)
-		<< "Divided up shutter, analogue gain and digital gain are "
-		<< shutterTime << ", " << aGain << " and " << dGain;
+		<< "Divided up shutter, frame duration, analogue gain and digital gain are "
+		<< shutterTime << ", " << frameDuration << ", " << aGain
+		<< " and " << dGain;
+
+	/* Use the frame duration to calculate the desired vblank. */
+	utils::Duration lineDuration = configuration.sensor.lineDuration;
+	IPACameraSensorInfo &sensorInfo = context.sensorInfo;
+
+	frameContext.agc.vblank = (frameDuration / lineDuration)
+				- sensorInfo.outputSize.height;
 
-	activeState.agc.automatic.exposure = shutterTime / configuration.sensor.lineDuration;
+	/* Populate the active state. */
+	activeState.agc.automatic.exposure = shutterTime / lineDuration;
 	activeState.agc.automatic.sensorGain = aGain;
 	activeState.agc.automatic.ispGain = dGain;
 
+	metadata.set(controls::FrameDuration, frameDuration.get<std::micro>());
 	metadata.set(controls::ExposureTime, currentShutter.get<std::micro>());
 	metadata.set(controls::AnalogueGain, frameContext.agc.sensorGain);
 	metadata.set(controls::DigitalGain, frameContext.agc.ispGain);
diff --git a/src/ipa/mali-c55/ipa_context.h b/src/ipa/mali-c55/ipa_context.h
index ecb2f79c0dca0e41166b88f2608c22aa72adcf8a..107e55a2dab325e8d360da1281b212aa052ec724 100644
--- a/src/ipa/mali-c55/ipa_context.h
+++ b/src/ipa/mali-c55/ipa_context.h
@@ -53,6 +53,8 @@ struct IPAActiveState {
 		uint32_t constraintMode;
 		uint32_t exposureMode;
 		uint32_t temperatureK;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
 	} agc;
 
 	struct {
@@ -66,6 +68,9 @@ struct IPAFrameContext : public FrameContext {
 		uint32_t exposure;
 		double sensorGain;
 		double ispGain;
+		uint32_t vblank;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
 	} agc;
 
 	struct {
@@ -80,6 +85,7 @@ struct IPAContext {
 	{
 	}
 
+	IPACameraSensorInfo sensorInfo;
 	IPASessionConfiguration configuration;
 	IPAActiveState activeState;
 
diff --git a/src/ipa/mali-c55/mali-c55.cpp b/src/ipa/mali-c55/mali-c55.cpp
index 12cad7374520db8c6fa5ca233a0ef33dc7b2f287..7454e4b0d7c8703398cdb716f9a52ea551784371 100644
--- a/src/ipa/mali-c55/mali-c55.cpp
+++ b/src/ipa/mali-c55/mali-c55.cpp
@@ -68,7 +68,7 @@ private:
 	void updateControls(const IPACameraSensorInfo &sensorInfo,
 			    const ControlInfoMap &sensorControls,
 			    ControlInfoMap *ipaControls);
-	void setControls();
+	void setControls(unsigned int frame);
 
 	std::map<unsigned int, MappedFrameBuffer> buffers_;
 
@@ -126,14 +126,17 @@ int IPAMaliC55::init(const IPASettings &settings, const IPAConfigInfo &ipaConfig
 	if (ret)
 		return ret;
 
+	context_.sensorInfo = ipaConfig.sensorInfo;
 	updateControls(ipaConfig.sensorInfo, ipaConfig.sensorControls, ipaControls);
 
 	return 0;
 }
 
-void IPAMaliC55::setControls()
+void IPAMaliC55::setControls(unsigned int frame)
 {
+	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
 	IPAActiveState &activeState = context_.activeState;
+	uint32_t vblank = frameContext.agc.vblank;
 	uint32_t exposure;
 	uint32_t gain;
 
@@ -148,6 +151,7 @@ void IPAMaliC55::setControls()
 	ControlList ctrls(sensorControls_);
 	ctrls.set(V4L2_CID_EXPOSURE, static_cast<int32_t>(exposure));
 	ctrls.set(V4L2_CID_ANALOGUE_GAIN, static_cast<int32_t>(gain));
+	ctrls.set(V4L2_CID_VBLANK, static_cast<int32_t>(vblank));
 
 	setSensorControls.emit(ctrls);
 }
@@ -368,7 +372,7 @@ void IPAMaliC55::processStats(unsigned int request, unsigned int bufferId,
 		algo->process(context_, request, frameContext, stats, metadata);
 	}
 
-	setControls();
+	setControls(request);
 
 	statsProcessed.emit(request, metadata);
 }
