diff --git a/src/ipa/mali-c55/algorithms/agc.cpp b/src/ipa/mali-c55/algorithms/agc.cpp
index a0b55694aad292f8a080d8266470797ac0cc2c25..92a58417f9cdf11aa1cd7ec7167bf4818060c85e 100644
--- a/src/ipa/mali-c55/algorithms/agc.cpp
+++ b/src/ipa/mali-c55/algorithms/agc.cpp
@@ -181,9 +181,10 @@ int Agc::configure(IPAContext &context,
 	sensorConfig.minAnalogueGain = context.configuration.sensor.minAnalogueGain;
 	sensorConfig.maxAnalogueGain = context.configuration.sensor.maxAnalogueGain;
 
-	AgcMeanLuminance::configure(sensorConfig, context.camHelper.get());
-
-	/* \todo Update AGC limits when FrameDurationLimits is passed in */
+	context.activeState.agc.maxFrameDuration =
+		AgcMeanLuminance::configure(sensorConfig, context.camHelper.get());
+	context.activeState.agc.minFrameDuration =
+		context.configuration.sensor.minFrameDuration;
 
 	return 0;
 }
@@ -209,6 +210,27 @@ 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 sensor constraints. */
+		int64_t sensorMinFrameDuration =
+			context.configuration.sensor.minFrameDuration.get<std::micro>();
+		int64_t sensorMaxFrameDuration =
+			context.configuration.sensor.maxFrameDuration.get<std::micro>();
+
+		int64_t minFrameDuration =
+			std::clamp((*frameDurationLimits).front(),
+				   sensorMinFrameDuration, sensorMaxFrameDuration);
+		int64_t maxFrameDuration =
+			std::clamp((*frameDurationLimits).back(),
+				   sensorMinFrameDuration, sensorMaxFrameDuration);
+
+		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...
@@ -372,6 +394,14 @@ void Agc::process(IPAContext &context,
 		return;
 	}
 
+	/*
+	 * Update the AGC limits using the frame duration.
+	 *
+	 * \todo Handle ExposureTime and AnalogueGain controls to support
+	 * manual mode.
+	 */
+	setExposureLimits({}, {}, frameContext.agc.maxFrameDuration, {});
+
 	statistics_.parseStatistics(stats);
 	context.activeState.agc.temperatureK = estimateCCT({ { statistics_.rHist.interQuantileMean(0, 1),
 							       statistics_.gHist.interQuantileMean(0, 1),
@@ -401,11 +431,21 @@ void Agc::process(IPAContext &context,
 		<< "Divided up shutter, analogue gain and digital gain are "
 		<< shutterTime << ", " << aGain << " and " << dGain;
 
-	activeState.agc.automatic.exposure = shutterTime / configuration.sensor.lineDuration;
+	/* Use the frame duration to calculate the desired vblank. */
+	utils::Duration lineDuration = configuration.sensor.lineDuration;
+	utils::Duration frameDuration =
+		context.camHelper->minFrameDuration(shutterTime, lineDuration);
+
+	frameContext.agc.vblank = (frameDuration / lineDuration)
+				- context.sensorInfo.outputSize.height;
+
+	/* Populate the active state. */
+	activeState.agc.automatic.exposure = shutterTime / lineDuration;
 	activeState.agc.automatic.sensorGain = aGain;
 	activeState.agc.automatic.ispGain = dGain;
 
-	metadata.set(controls::ExposureTime, currentShutter.get<std::micro>());
+	metadata.set(controls::FrameDuration, frameDuration.get<std::micro>());
+	metadata.set(controls::ExposureTime, shutterTime.get<std::micro>());
 	metadata.set(controls::AnalogueGain, frameContext.agc.sensorGain);
 	metadata.set(controls::DigitalGain, frameContext.agc.ispGain);
 	metadata.set(controls::ColourTemperature, context.activeState.agc.temperatureK);
diff --git a/src/ipa/mali-c55/ipa_context.h b/src/ipa/mali-c55/ipa_context.h
index 828103f21451d9f7f4998c3faedc8fb6a1e7a2ec..4b76ac25ec4a2e1d2e07642148547303cf4c6031 100644
--- a/src/ipa/mali-c55/ipa_context.h
+++ b/src/ipa/mali-c55/ipa_context.h
@@ -10,6 +10,8 @@
 #include <libcamera/base/utils.h>
 #include <libcamera/controls.h>
 
+#include <libcamera/ipa/core_ipa_interface.h>
+
 #include "libcamera/internal/bayer_format.h"
 
 #include <libipa/camera_sensor_helper.h>
@@ -67,6 +69,9 @@ struct IPAFrameContext : public FrameContext {
 		uint32_t exposure;
 		double sensorGain;
 		double ispGain;
+		uint32_t vblank;
+		utils::Duration minFrameDuration;
+		utils::Duration maxFrameDuration;
 	} agc;
 
 	struct {
@@ -81,6 +86,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 02f5dfb76eae073858ec688746b7e12ec072e567..60b5ee8d3060e9f3a4794550fe4140d58125a925 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);
 }
@@ -375,7 +379,7 @@ void IPAMaliC55::processStats(unsigned int request, unsigned int bufferId,
 		algo->process(context_, request, frameContext, stats, metadata);
 	}
 
-	setControls();
+	setControls(request);
 
 	statsProcessed.emit(request, metadata);
 }
