diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h
index 82d98184ed..e97c0f9bf8 100644
--- a/include/libcamera/internal/v4l2_videodevice.h
+++ b/include/libcamera/internal/v4l2_videodevice.h
@@ -209,6 +209,9 @@ public:
 	Formats formats(uint32_t code = 0);
 
 	int getFrameInterval(std::chrono::microseconds *interval);
+	int setFrameInterval(std::chrono::microseconds *interval);
+	std::optional<std::array<std::chrono::microseconds, 2>>
+	getFrameIntervalLimits(V4L2PixelFormat pixelFormat, Size size);
 
 	int getSelection(unsigned int target, Rectangle *rect);
 	int setSelection(unsigned int target, Rectangle *rect);
diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
index 3f98e8ece0..e4e9b8ab9b 100644
--- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
@@ -57,6 +57,7 @@ public:
 	std::unique_ptr<V4L2VideoDevice> video_;
 	Stream stream_;
 	std::map<PixelFormat, std::vector<SizeRange>> formats_;
+	std::map<std::pair<PixelFormat, Size>, std::array<std::chrono::microseconds, 2>> frameIntervals_;
 
 	std::optional<v4l2_exposure_auto_type> autoExposureMode_;
 	std::optional<v4l2_exposure_auto_type> manualExposureMode_;
@@ -277,6 +278,22 @@ int PipelineHandlerUVC::configure(Camera *camera, CameraConfiguration *config)
 	    format.fourcc != data->video_->toV4L2PixelFormat(cfg.pixelFormat))
 		return -EINVAL;
 
+	auto it = data->controlInfo_.find(&controls::FrameDurationLimits);
+	if (it != data->controlInfo_.end()) {
+		auto it2 = data->frameIntervals_.find({ cfg.pixelFormat, cfg.size });
+		if (it2 != data->frameIntervals_.end()) {
+			std::chrono::microseconds current;
+
+			ret = data->video_->getFrameInterval(&current);
+
+			it->second = ControlInfo{
+				int64_t(it2->second[0].count()),
+				int64_t(it2->second[1].count()),
+				ret == 0 ? ControlValue(int64_t(current.count())) : ControlValue()
+			};
+		}
+	}
+
 	cfg.setStream(&data->stream_);
 
 	return 0;
@@ -306,6 +323,19 @@ int PipelineHandlerUVC::start(Camera *camera, const ControlList *controls)
 		ret = processControls(data, *controls);
 		if (ret < 0)
 			goto err_release_buffers;
+
+		/* Can only be set before starting. */
+		auto fdl = controls->get(controls::FrameDurationLimits);
+		if (fdl) {
+			const auto wantMin = std::chrono::microseconds((*fdl)[0]);
+			const auto wantMax = std::chrono::microseconds((*fdl)[1]);
+			auto want = (wantMin + wantMax) / 2;
+
+			/* Let the kernel choose something close to the middle. */
+			ret = data->video_->setFrameInterval(&want);
+			if (ret == 0)
+				data->timePerFrame_ = want;
+		}
 	}
 
 	ret = data->video_->streamOn();
@@ -355,6 +385,8 @@ int PipelineHandlerUVC::processControl(const UVCCameraData *data, ControlList *c
 		cid = V4L2_CID_GAMMA;
 	else if (id == controls::AeEnable)
 		return 0; /* Handled in `Camera::queueRequest()`. */
+	else if (id == controls::FrameDurationLimits)
+		return 0; /* Handled in `start()` */
 	else
 		return -EINVAL;
 
@@ -555,6 +587,23 @@ int UVCCameraData::init(std::shared_ptr<MediaDevice> media)
 	 * resolution from the largest size it advertises.
 	 */
 	Size resolution;
+	auto minFrameInterval = std::chrono::microseconds::max();
+	auto maxFrameInterval = std::chrono::microseconds::min();
+
+	const auto processFrameIntervals = [&](PixelFormat pf, V4L2PixelFormat v4l2pf, SizeRange size) {
+		if (size.min != size.max)
+			return;
+
+		auto frameIntervals = video_->getFrameIntervalLimits(v4l2pf, size.min);
+		if (!frameIntervals)
+			return;
+
+		minFrameInterval = std::min(minFrameInterval, (*frameIntervals)[0]);
+		maxFrameInterval = std::max(maxFrameInterval, (*frameIntervals)[1]);
+
+		frameIntervals_.try_emplace({ pf, size.min }, *frameIntervals);
+	};
+
 	for (const auto &format : video_->formats()) {
 		PixelFormat pixelFormat = format.first.toPixelFormat();
 		if (!pixelFormat.isValid())
@@ -566,6 +615,8 @@ int UVCCameraData::init(std::shared_ptr<MediaDevice> media)
 		for (const SizeRange &sizeRange : sizeRanges) {
 			if (sizeRange.max > resolution)
 				resolution = sizeRange.max;
+
+			processFrameIntervals(pixelFormat, format.first, sizeRange);
 		}
 	}
 
@@ -625,6 +676,14 @@ int UVCCameraData::init(std::shared_ptr<MediaDevice> media)
 		ctrls[&controls::AeEnable] = ControlInfo(false, true, true);
 	}
 
+	/* Use the global min/max here, limits will be updated in `configure()`. */
+	if (!frameIntervals_.empty()) {
+		ctrls[&controls::FrameDurationLimits] = ControlInfo{
+			int64_t(minFrameInterval.count()),
+			int64_t(maxFrameInterval.count()),
+		};
+	}
+
 	controlInfo_ = ControlInfoMap(std::move(ctrls), controls::controls);
 
 	/*
diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp
index 3836dabef3..3db6e80aed 100644
--- a/src/libcamera/v4l2_videodevice.cpp
+++ b/src/libcamera/v4l2_videodevice.cpp
@@ -1202,6 +1202,122 @@ int V4L2VideoDevice::getFrameInterval(std::chrono::microseconds *interval)
 	return 0;
 }
 
+/**
+ * \brief Configure the frame interval
+ * \param[inout] interval The frame interval
+ *
+ * Apply the supplied \a interval as the time-per-frame stream parameter
+ * on the device, and return the actually applied value.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2VideoDevice::setFrameInterval(std::chrono::microseconds *interval)
+{
+	v4l2_fract *frameInterval = nullptr;
+	uint32_t *caps = nullptr;
+	v4l2_streamparm sparm = {};
+
+	sparm.type = bufferType_;
+
+	switch (sparm.type) {
+	case V4L2_BUF_TYPE_VIDEO_CAPTURE:
+	case V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE:
+		frameInterval = &sparm.parm.capture.timeperframe;
+		caps = &sparm.parm.capture.capability;
+		break;
+	case V4L2_BUF_TYPE_VIDEO_OUTPUT:
+	case V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE:
+		frameInterval = &sparm.parm.output.timeperframe;
+		caps = &sparm.parm.output.capability;
+		break;
+	}
+
+	if (!frameInterval)
+		return -EINVAL;
+
+	constexpr auto max = std::numeric_limits<decltype(frameInterval->numerator)>::max();
+	if (interval->count() <= 0 || interval->count() > max)
+		return -EINVAL;
+
+	frameInterval->numerator = interval->count();
+	frameInterval->denominator = std::chrono::microseconds(std::chrono::seconds(1)).count();
+
+	int ret = ioctl(VIDIOC_S_PARM, &sparm);
+	if (ret)
+		return ret;
+
+	if (!(*caps & V4L2_CAP_TIMEPERFRAME))
+		return -ENOTSUP;
+
+	*interval = v4l2FractionToMs(*frameInterval);
+
+	return 0;
+}
+
+/**
+ * \brief Retrieve the frame interval limits
+ * \param[in] pixelFormat The pixel format
+ * \param[in] size The size
+ *
+ * Retrieve the minimum and maximum available frame interval for
+ * the given \a pixelFormat and \a size.
+ *
+ * \return The min and max frame interval or std::nullopt otherwise
+ */
+std::optional<std::array<std::chrono::microseconds, 2>>
+V4L2VideoDevice::getFrameIntervalLimits(V4L2PixelFormat pixelFormat, Size size)
+{
+	auto min = std::chrono::microseconds::max();
+	auto max = std::chrono::microseconds::min();
+	unsigned int index = 0;
+	int ret;
+
+	for (;; index++) {
+		struct v4l2_frmivalenum frameInterval = {};
+		frameInterval.index = index;
+		frameInterval.pixel_format = pixelFormat;
+		frameInterval.width = size.width;
+		frameInterval.height = size.height;
+
+		ret = ioctl(VIDIOC_ENUM_FRAMEINTERVALS, &frameInterval);
+		if (ret)
+			break;
+
+		switch (frameInterval.type) {
+		case V4L2_FRMIVAL_TYPE_DISCRETE: {
+			auto ms = v4l2FractionToMs(frameInterval.discrete);
+
+			min = std::min(min, ms);
+			max = std::max(max, ms);
+			break;
+		}
+		case V4L2_FRMIVAL_TYPE_CONTINUOUS:
+		case V4L2_FRMIVAL_TYPE_STEPWISE: {
+			min = std::min(min, v4l2FractionToMs(frameInterval.stepwise.min));
+			max = std::max(max, v4l2FractionToMs(frameInterval.stepwise.max));
+			break;
+		}
+		default:
+			LOG(V4L2, Error)
+				<< "Unknown v4l2_frmsizetypes value "
+				<< frameInterval.type;
+			return {};
+		}
+	}
+
+	if (ret && ret != -EINVAL) {
+		LOG(V4L2, Error)
+			<< "Unable to enumerate pixel formats: "
+			<< strerror(-ret);
+		return {};
+	}
+
+	if (index <= 0)
+		return {};
+
+	return {{ min, max }};
+}
+
 std::vector<V4L2PixelFormat> V4L2VideoDevice::enumPixelformats(uint32_t code)
 {
 	std::vector<V4L2PixelFormat> formats;
