diff --git a/src/libcamera/pipeline/ipu3/frames.cpp b/src/libcamera/pipeline/ipu3/frames.cpp
new file mode 100644
index 0000000000000000..37932dc779a5a112
--- /dev/null
+++ b/src/libcamera/pipeline/ipu3/frames.cpp
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * frames.cpp - Intel IPU3 Frames helper
+ */
+
+#include "frames.h"
+
+#include <libcamera/buffer.h>
+#include <libcamera/request.h>
+
+#include "libcamera/internal/pipeline_handler.h"
+#include "libcamera/internal/v4l2_videodevice.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(IPU3)
+
+IPU3Frames::IPU3Frames()
+	: nextId_(0)
+{
+}
+
+void IPU3Frames::init(const std::vector<std::unique_ptr<FrameBuffer>> &paramBuffers,
+		      const std::vector<std::unique_ptr<FrameBuffer>> &statBuffers)
+{
+	for (const std::unique_ptr<FrameBuffer> &buffer : paramBuffers)
+		availableParamBuffers_.push(buffer.get());
+
+	for (const std::unique_ptr<FrameBuffer> &buffer : statBuffers)
+		availableStatBuffers_.push(buffer.get());
+
+	nextId_ = 0;
+	frameInfo_.clear();
+}
+
+void IPU3Frames::clear()
+{
+	availableParamBuffers_ = {};
+	availableStatBuffers_ = {};
+}
+
+IPU3Frames::Info *IPU3Frames::create(Request *request)
+{
+	unsigned int id = nextId_++;
+
+	if (availableParamBuffers_.empty()) {
+		LOG(IPU3, Error) << "Parameters buffer underrun";
+		return nullptr;
+	}
+	FrameBuffer *paramBuffer = availableParamBuffers_.front();
+
+	if (availableStatBuffers_.empty()) {
+		LOG(IPU3, Error) << "Statisitc buffer underrun";
+		return nullptr;
+	}
+	FrameBuffer *statBuffer = availableStatBuffers_.front();
+
+	availableParamBuffers_.pop();
+	availableStatBuffers_.pop();
+
+	/* \todo Remove the dynamic allocation of Info */
+	std::unique_ptr<Info> info = std::make_unique<Info>();
+
+	info->id = id;
+	info->request = request;
+	info->rawBuffer = nullptr;
+	info->paramBuffer = paramBuffer;
+	info->statBuffer = statBuffer;
+	info->paramDequeued = false;
+	info->metadataProcessed = false;
+
+	frameInfo_[id] = std::move(info);
+
+	return frameInfo_[id].get();
+}
+
+void IPU3Frames::remove(IPU3Frames::Info *info)
+{
+	/* Return params and stat buffer for reuse. */
+	availableParamBuffers_.push(info->paramBuffer);
+	availableStatBuffers_.push(info->statBuffer);
+
+	/* Delete the extended frame information. */
+	frameInfo_.erase(info->id);
+}
+
+bool IPU3Frames::tryComplete(IPU3Frames::Info *info)
+{
+	Request *request = info->request;
+
+	if (request->hasPendingBuffers())
+		return false;
+
+	if (!info->metadataProcessed)
+		return false;
+
+	if (!info->paramDequeued)
+		return false;
+
+	remove(info);
+
+	return true;
+}
+
+IPU3Frames::Info *IPU3Frames::find(unsigned int id)
+{
+	const auto &itInfo = frameInfo_.find(id);
+
+	if (itInfo != frameInfo_.end())
+		return itInfo->second.get();
+
+	LOG(IPU3, Error) << "Can't find tracking informaton for frame " << id;
+	return nullptr;
+}
+
+IPU3Frames::Info *IPU3Frames::find(FrameBuffer *buffer)
+{
+	for (auto const &itInfo : frameInfo_) {
+		Info *info = itInfo.second.get();
+
+		for (auto const itBuffers : info->request->buffers())
+			if (itBuffers.second == buffer)
+				return info;
+
+		if (info->rawBuffer == buffer || info->paramBuffer == buffer ||
+		    info->statBuffer == buffer)
+			return info;
+	}
+
+	LOG(IPU3, Error) << "Can't find tracking informaton from buffer";
+	return nullptr;
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/ipu3/frames.h b/src/libcamera/pipeline/ipu3/frames.h
new file mode 100644
index 0000000000000000..106e5c15cc7aea9b
--- /dev/null
+++ b/src/libcamera/pipeline/ipu3/frames.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * frames.h - Intel IPU3 Frames helper
+ */
+#ifndef __LIBCAMERA_PIPELINE_IPU3_FRAMES_H__
+#define __LIBCAMERA_PIPELINE_IPU3_FRAMES_H__
+
+#include <map>
+#include <memory>
+#include <queue>
+#include <vector>
+
+namespace libcamera {
+
+class FrameBuffer;
+class IPAProxy;
+class PipelineHandler;
+class Request;
+class V4L2VideoDevice;
+struct IPABuffer;
+
+class IPU3Frames
+{
+public:
+	struct Info {
+		unsigned int id;
+		Request *request;
+
+		FrameBuffer *rawBuffer;
+		FrameBuffer *paramBuffer;
+		FrameBuffer *statBuffer;
+
+		bool paramDequeued;
+		bool metadataProcessed;
+	};
+
+	IPU3Frames();
+
+	void init(const std::vector<std::unique_ptr<FrameBuffer>> &paramBuffers,
+		  const std::vector<std::unique_ptr<FrameBuffer>> &statBuffers);
+	void clear();
+
+	Info *create(Request *request);
+	void remove(Info *info);
+	bool tryComplete(Info *info);
+
+	Info *find(unsigned int id);
+	Info *find(FrameBuffer *buffer);
+
+private:
+	std::queue<FrameBuffer *> availableParamBuffers_;
+	std::queue<FrameBuffer *> availableStatBuffers_;
+
+	unsigned int nextId_;
+	std::map<unsigned int, std::unique_ptr<Info>> frameInfo_;
+};
+
+} /* namespace libcamera */
+
+#endif /* __LIBCAMERA_PIPELINE_IPU3_FRAMES_H__ */
diff --git a/src/libcamera/pipeline/ipu3/meson.build b/src/libcamera/pipeline/ipu3/meson.build
index d60e07ae6ccac2bc..a1b0b31ac5bcf864 100644
--- a/src/libcamera/pipeline/ipu3/meson.build
+++ b/src/libcamera/pipeline/ipu3/meson.build
@@ -2,6 +2,7 @@
 
 libcamera_sources += files([
     'cio2.cpp',
+    'frames.cpp',
     'imgu.cpp',
     'ipu3.cpp',
 ])
