diff --git a/include/libcamera/request.h b/include/libcamera/request.h
index 7a60be617645..00c68345b5b4 100644
--- a/include/libcamera/request.h
+++ b/include/libcamera/request.h
@@ -51,7 +51,7 @@ private:
 	friend class PipelineHandler;
 
 	int prepare();
-	void complete(Status status);
+	void complete();
 
 	bool completeBuffer(Buffer *buffer);
 
@@ -62,6 +62,7 @@ private:
 
 	uint64_t cookie_;
 	Status status_;
+	bool cancelled_;
 };
 
 } /* namespace libcamera */
diff --git a/src/libcamera/include/pipeline_handler.h b/src/libcamera/include/pipeline_handler.h
index f836d5d1a600..1fdef9cea40f 100644
--- a/src/libcamera/include/pipeline_handler.h
+++ b/src/libcamera/include/pipeline_handler.h
@@ -74,7 +74,7 @@ public:
 				const std::set<Stream *> &streams) = 0;
 
 	virtual int start(Camera *camera) = 0;
-	virtual void stop(Camera *camera);
+	virtual void stop(Camera *camera) = 0;
 
 	virtual int queueRequest(Camera *camera, Request *request);
 
diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index 50457891e288..ffc066a15ae9 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -711,8 +711,6 @@ void PipelineHandlerIPU3::stop(Camera *camera)
 	if (ret)
 		LOG(IPU3, Warning) << "Failed to stop camera "
 				   << camera->name();
-
-	PipelineHandler::stop(camera);
 }
 
 int PipelineHandlerIPU3::queueRequest(Camera *camera, Request *request)
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
index 383236435a7f..cc33a2cb2572 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
+++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
@@ -354,8 +354,6 @@ void PipelineHandlerRkISP1::stop(Camera *camera)
 		LOG(RkISP1, Warning)
 			<< "Failed to stop camera " << camera->name();
 
-	PipelineHandler::stop(camera);
-
 	activeCamera_ = nullptr;
 }
 
diff --git a/src/libcamera/pipeline/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo.cpp
index 387d71ef567c..b299c5da8471 100644
--- a/src/libcamera/pipeline/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo.cpp
@@ -220,7 +220,6 @@ void PipelineHandlerUVC::stop(Camera *camera)
 {
 	UVCCameraData *data = cameraData(camera);
 	data->video_->streamOff();
-	PipelineHandler::stop(camera);
 }
 
 int PipelineHandlerUVC::processControls(UVCCameraData *data, Request *request)
diff --git a/src/libcamera/pipeline/vimc.cpp b/src/libcamera/pipeline/vimc.cpp
index ff2529c974fd..7d96c3645c06 100644
--- a/src/libcamera/pipeline/vimc.cpp
+++ b/src/libcamera/pipeline/vimc.cpp
@@ -222,7 +222,6 @@ void PipelineHandlerVimc::stop(Camera *camera)
 {
 	VimcCameraData *data = cameraData(camera);
 	data->video_->streamOff();
-	PipelineHandler::stop(camera);
 }
 
 int PipelineHandlerVimc::processControls(VimcCameraData *data, Request *request)
diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp
index c609200252f1..83938a71f615 100644
--- a/src/libcamera/pipeline_handler.cpp
+++ b/src/libcamera/pipeline_handler.cpp
@@ -328,31 +328,7 @@ const ControlInfoMap &PipelineHandler::controls(Camera *camera)
  *
  * This method stops capturing and processing requests immediately. All pending
  * requests are cancelled and complete immediately in an error state.
- *
- * Pipeline handlers shall override this method to stop the pipeline, ensure
- * that all pending request completion signaled through completeRequest() have
- * returned, and call the base implementation of the stop() method as the last
- * step of their implementation. The base implementation cancels all requests
- * queued but not yet complete.
  */
-void PipelineHandler::stop(Camera *camera)
-{
-	CameraData *data = cameraData(camera);
-
-	while (!data->queuedRequests_.empty()) {
-		Request *request = data->queuedRequests_.front();
-		data->queuedRequests_.pop_front();
-
-		while (!request->pending_.empty()) {
-			Buffer *buffer = *request->pending_.begin();
-			buffer->cancel();
-			completeBuffer(camera, request, buffer);
-		}
-
-		request->complete(Request::RequestCancelled);
-		camera->requestComplete(request);
-	}
-}
 
 /**
  * \fn PipelineHandler::queueRequest()
@@ -420,15 +396,16 @@ bool PipelineHandler::completeBuffer(Camera *camera, Request *request,
  */
 void PipelineHandler::completeRequest(Camera *camera, Request *request)
 {
-	request->complete(Request::RequestComplete);
+	request->complete();
 
 	CameraData *data = cameraData(camera);
 
 	while (!data->queuedRequests_.empty()) {
 		request = data->queuedRequests_.front();
-		if (request->hasPendingBuffers())
+		if (request->status() == Request::RequestPending)
 			break;
 
+		ASSERT(!request->hasPendingBuffers());
 		data->queuedRequests_.pop_front();
 		camera->requestComplete(request);
 	}
diff --git a/src/libcamera/request.cpp b/src/libcamera/request.cpp
index 45e7133febb0..19131472710b 100644
--- a/src/libcamera/request.cpp
+++ b/src/libcamera/request.cpp
@@ -56,7 +56,7 @@ LOG_DEFINE_CATEGORY(Request)
  */
 Request::Request(Camera *camera, uint64_t cookie)
 	: camera_(camera), controls_(camera), cookie_(cookie),
-	  status_(RequestPending)
+	  status_(RequestPending), cancelled_(false)
 {
 }
 
@@ -199,14 +199,15 @@ int Request::prepare()
 
 /**
  * \brief Complete a queued request
- * \param[in] status The request completion status
  *
- * Mark the request as complete by updating its status to \a status.
+ * Mark the request as complete by updating its status to RequestComplete,
+ * unless buffers have been cancelled in which case the status is set to
+ * RequestCancelled.
  */
-void Request::complete(Status status)
+void Request::complete()
 {
 	ASSERT(!hasPendingBuffers());
-	status_ = status;
+	status_ = cancelled_ ? RequestCancelled : RequestComplete;
 }
 
 /**
@@ -229,6 +230,9 @@ bool Request::completeBuffer(Buffer *buffer)
 
 	buffer->setRequest(nullptr);
 
+	if (buffer->status() == Buffer::BufferCancelled)
+		cancelled_ = true;
+
 	return !hasPendingBuffers();
 }
 
diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp
index 65b4098abc05..aa3136378997 100644
--- a/src/libcamera/v4l2_videodevice.cpp
+++ b/src/libcamera/v4l2_videodevice.cpp
@@ -1090,7 +1090,9 @@ int V4L2VideoDevice::streamOn()
  * \brief Stop the video stream
  *
  * Buffers that are still queued when the video stream is stopped are
- * implicitly dequeued, but no bufferReady signal is emitted for them.
+ * immediately dequeued with their status set to Buffer::BufferError,
+ * and the bufferReady signal is emitted for them. The order in which those
+ * buffers are dequeued is not specified.
  *
  * \return 0 on success or a negative error code otherwise
  */
@@ -1105,6 +1107,16 @@ int V4L2VideoDevice::streamOff()
 		return ret;
 	}
 
+	/* Send back all queued buffers. */
+	for (auto it : queuedBuffers_) {
+		unsigned int index = it.first;
+		Buffer *buffer = it.second;
+
+		buffer->index_ = index;
+		buffer->cancel();
+		bufferReady.emit(buffer);
+	}
+
 	queuedBuffers_.clear();
 	fdEvent_->setEnabled(false);
 
diff --git a/test/v4l2_videodevice/buffer_sharing.cpp b/test/v4l2_videodevice/buffer_sharing.cpp
index 459bd238fe97..4da6ba466f40 100644
--- a/test/v4l2_videodevice/buffer_sharing.cpp
+++ b/test/v4l2_videodevice/buffer_sharing.cpp
@@ -95,6 +95,9 @@ protected:
 		std::cout << "Received capture buffer: " << buffer->index()
 			  << " sequence " << buffer->sequence() << std::endl;
 
+		if (buffer->status() != Buffer::BufferSuccess)
+			return;
+
 		output_->queueBuffer(buffer);
 		framesCaptured_++;
 	}
@@ -104,6 +107,9 @@ protected:
 		std::cout << "Received output buffer: " << buffer->index()
 			  << " sequence " << buffer->sequence() << std::endl;
 
+		if (buffer->status() != Buffer::BufferSuccess)
+			return;
+
 		capture_->queueBuffer(buffer);
 		framesOutput_++;
 	}
