diff --git a/include/libcamera/internal/camera.h b/include/libcamera/internal/camera.h
index 31914075f0..13b72258ff 100644
--- a/include/libcamera/internal/camera.h
+++ b/include/libcamera/internal/camera.h
@@ -59,6 +59,12 @@ public:
 	using PooledFrameBuffer = std::unique_ptr<FrameBuffer, FrameBufferPoolDeleter>;
 
 	[[nodiscard]] PooledFrameBuffer acquireBuffer(const Stream *stream);
+	void rejectBuffer(FrameBuffer *buffer);
+
+	void rejectBuffer(PooledFrameBuffer buffer)
+	{
+		return rejectBuffer(buffer.release());
+	}
 
 private:
 	enum State {
diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp
index 016f36e796..be55ed72c4 100644
--- a/src/libcamera/camera.cpp
+++ b/src/libcamera/camera.cpp
@@ -790,6 +790,38 @@ Camera::Private::acquireBuffer(const Stream *stream)
 	it->second.buffers.pop_back();
 	return { buffer, { &it->second.buffers } };
 }
+
+/**
+ * \brief Return a buffer to the application
+ * \param[in] buffer The buffer
+ *
+ * \note \a buffer must not be in the camera's buffer pool
+ * \todo return fence on timeout?
+ */
+void Camera::Private::rejectBuffer(FrameBuffer *buffer)
+{
+	ASSERT(!buffer->_d()->request());
+	ASSERT(buffer->_d()->stream_);
+
+	LOG(Camera, Debug)
+		<< "Camera:" << LIBCAMERA_O_PTR() << " rejects buffer:"
+		<< buffer << " for stream:" << buffer->_d()->stream_;
+
+	/*
+	 * \todo Not `FrameError` because that requires `timestamp` and
+	 * `sequence` to be valid.
+	 */
+	buffer->_d()->cancel();
+	buffer->_d()->stream_ = nullptr;
+
+	// \todo separate event (with stream) ?
+	LIBCAMERA_O_PTR()->bufferCompleted.emit(nullptr, buffer);
+}
+
+/**
+ * \fn Camera::Private::rejectBuffer(PooledFrameBuffer buffer)
+ * \copydoc Camera::Private::rejectBuffer(FrameBuffer *buffer)
+ */
 #endif /* __DOXYGEN_PUBLIC__ */
 
 /**
