diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp
index 90f51ff035..a8eb74c680 100644
--- a/src/ipa/libipa/agc.cpp
+++ b/src/ipa/libipa/agc.cpp
@@ -72,12 +72,12 @@ namespace agc {
  * \struct Session
  * \brief Session configuration for AgcAlgorithm
  *
+ * \var Session::minExposure
+ * \brief Minimum exposure (in lines) for the streaming session
+ *
  * \var Session::minExposureTime
  * \brief Minimum exposure time for the streaming session
  *
- * \var Session::maxExposureTime
- * \brief Maximum exposure time for the streaming session
- *
  * \var Session::minAnalogueGain
  * \brief Minimum analogue gain for the streaming session
  *
@@ -102,6 +102,11 @@ namespace agc {
  * \var Session::sensor.outputSize
  * \brief Configured output size of the sensor
  *
+ * \var Session::sensor.exposureMargin
+ * \brief Exposure margin of the sensor
+ *
+ * \sa CameraSensorHelper::exposureMargin()
+ *
  * \var Session::autoAllowed
  * \copybrief AgcAlgorithm::ConfigurationParams::autoAllowed
  * \sa AgcAlgorithm::ConfigurationParams::autoAllowed
@@ -473,6 +478,23 @@ void fillMissing(AgcMeanLuminance::Configuration &config)
 	}
 }
 
+[[nodiscard]]
+uint32_t clampExposure(const agc::Session &session, const agc::ActiveState &state,
+		       uint32_t exposure)
+{
+	const uint32_t maxFrame = state.maxFrameDuration / session.lineDuration;
+	const uint32_t maxExposure = maxFrame - session.sensor.exposureMargin;
+
+	return std::clamp(exposure, session.minExposure, maxExposure);
+}
+
+[[nodiscard]]
+uint32_t clampExposure(const agc::Session &session, const agc::ActiveState &state,
+		       utils::Duration exposureTime)
+{
+	return clampExposure(session, state, exposureTime / session.lineDuration);
+}
+
 } /* namespace */
 
 /**
@@ -620,7 +642,12 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state,
 	session.lineDuration = lineLength * 1.0s / config.sensorInfo.pixelRate;
 	session.sensor.outputSize = config.sensorInfo.outputSize;
 
-	const double lineDurationUs = session.lineDuration.get<std::micro>();
+	auto exposureMargin = sensor_ ? sensor_->exposureMargin() : std::nullopt;
+	session.sensor.exposureMargin = exposureMargin.value_or(4);
+	if (!exposureMargin)
+		LOG(Agc, Warning)
+			<< "Sensor exposure margin not available, using "
+			<< session.sensor.exposureMargin;
 
 	/*
 	 * Compute exposure time limits from the V4L2_CID_EXPOSURE control
@@ -629,7 +656,6 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state,
 
 	const ControlInfo &v4l2Exposure = config.sensorControls.find(V4L2_CID_EXPOSURE)->second;
 	int32_t minExposure = v4l2Exposure.min().get<int32_t>();
-	int32_t maxExposure = v4l2Exposure.max().get<int32_t>();
 	int32_t defExposure = v4l2Exposure.def().get<int32_t>();
 
 	/* Compute the analogue gain limits. */
@@ -642,28 +668,46 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state,
 	float maxGain = extractGain(v4l2Gain.max());
 	float defGain = extractGain(v4l2Gain.def());
 
-	LOG(Agc, Debug)
-		<< "exposure: [" << minExposure << ',' << maxExposure << "], "
-		<< "gain: [" << minGain << ',' << maxGain << "], "
-		<< "line-duration: " << session.lineDuration << ", "
-		<< "sensor-output: " << session.sensor.outputSize;
-
-
 	/*
 	 * When the AGC computes the new exposure values for a frame, it needs
 	 * to know the limits for exposure time and analogue gain. As it depends
 	 * on the sensor, update it with the controls.
-	 *
-	 * \todo take VBLANK into account for maximum exposure time
 	 */
+	session.minExposure = minExposure;
 	session.minExposureTime = minExposure * session.lineDuration;
-	session.maxExposureTime = maxExposure * session.lineDuration;
 	session.minAnalogueGain = minGain;
 	session.maxAnalogueGain = maxGain;
 	session.defAnalogueGain = defGain;
 	session.minFrameDuration = config.sensorInfo.minFrameLength * session.lineDuration;
 	session.maxFrameDuration = config.sensorInfo.maxFrameLength * session.lineDuration;
 
+	/*
+	 * The assumption is that the minimum exposure time is independent of vblank,
+	 * only the maximum exposure time is affected by it.
+	 */
+	const uint32_t maxExposure = config.sensorInfo.maxFrameLength - session.sensor.exposureMargin;
+	const utils::Duration maxExposureTime = maxExposure * session.lineDuration;
+
+	LOG(Agc, Debug)
+		<< "exposure: [" << session.minExposure << ',' << maxExposure << "], "
+		<< "exposure-time: [" << session.minExposureTime << ',' << maxExposureTime << "], "
+		<< "gain: [" << session.minAnalogueGain << ',' << session.maxAnalogueGain << "], "
+		<< "line-length: " << lineLength << ", "
+		<< "line-duration: " << session.lineDuration << ", "
+		<< "frame-height: [" << config.sensorInfo.minFrameLength << ',' << config.sensorInfo.maxFrameLength << "], "
+		<< "sensor-output: " << session.sensor.outputSize << ", "
+		<< "sensor-exposure-margin: " << session.sensor.exposureMargin;
+
+	/*
+	 * This is a sanity check. If this fails, something is very likely off.
+	 * Equality could theoretically be allowed, but it is not so that
+	 * `(frameDuration / lineDuration) - exposureMargin` is no less
+	 * than the minimum exposure time even if the division is slightly
+	 * inaccurate.
+	 */
+	if (session.minExposure + session.sensor.exposureMargin >= config.sensorInfo.minFrameLength)
+		return -EINVAL;
+
 	/* Configure the default exposure and gain. */
 	state = {};
 	state.automatic.gain = session.minAnalogueGain;
@@ -755,9 +799,10 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state,
 		minGain, maxGain, defGain
 	};
 	config.ctrlMap[&controls::ExposureTime] = ControlInfo{
-		static_cast<int32_t>(minExposure * lineDurationUs),
-		static_cast<int32_t>(maxExposure * lineDurationUs),
-		static_cast<int32_t>(defExposure * lineDurationUs),
+		static_cast<int32_t>(session.minExposureTime.get<std::micro>()),
+		static_cast<int32_t>(maxExposureTime.get<std::micro>()),
+		static_cast<int32_t>(state.automatic.exposure *
+				     session.lineDuration.get<std::micro>()),
 	};
 	config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{
 		static_cast<int64_t>(session.minFrameDuration.get<std::micro>()),
@@ -864,7 +909,7 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s
 
 	const auto &exposure = controls.get(controls::ExposureTime);
 	if (exposure && !state.autoExposureEnabled) {
-		state.manual.exposure = *exposure * 1.0us / session.lineDuration;
+		state.manual.exposure = clampExposure(session, state, *exposure * 1.0us);
 
 		LOG(Agc, Debug) << "Set exposure to " << state.manual.exposure;
 	}
@@ -1002,9 +1047,8 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state,
 
 	if (state.autoExposureEnabled) {
 		minExposureTime = session.minExposureTime;
-		maxExposureTime = std::clamp(state.maxFrameDuration,
-					     session.minExposureTime,
-					     session.maxExposureTime);
+		maxExposureTime = state.maxFrameDuration -
+				  session.sensor.exposureMargin * lineDuration;
 	} else {
 		minExposureTime = lineDuration * state.manual.exposure;
 		maxExposureTime = minExposureTime;
@@ -1076,6 +1120,8 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state,
 		},
 	}, impl_);
 
+	state.automatic.exposure = clampExposure(session, state, state.automatic.exposure);
+
 	const utils::Duration newExposureTime = state.automatic.exposure * lineDuration;
 
 	LOG(Agc, Debug)
@@ -1106,7 +1152,7 @@ void AgcAlgorithm::processFrameDuration(const agc::Session &session,
 	const auto frameLength = std::max<uint32_t>(
 		std::min(std::ceil(frameContext.minFrameDuration / lineDuration),
 			 frameContext.maxFrameDuration / lineDuration),
-		frameContext.exposure);
+		frameContext.exposure + session.sensor.exposureMargin);
 
 	frameContext.vblank = frameLength - session.sensor.outputSize.height;
 
diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h
index 44f5218b0c..7bfc85189f 100644
--- a/src/ipa/libipa/agc.h
+++ b/src/ipa/libipa/agc.h
@@ -36,8 +36,8 @@ class Histogram;
 namespace agc {
 
 struct Session {
+	uint32_t minExposure;
 	utils::Duration minExposureTime;
-	utils::Duration maxExposureTime;
 	double minAnalogueGain;
 	double maxAnalogueGain;
 	double defAnalogueGain;
@@ -47,6 +47,7 @@ struct Session {
 
 	struct {
 		Size outputSize;
+		uint32_t exposureMargin;
 	} sensor;
 
 	bool autoAllowed;
