diff --git a/block/backup.c b/block/backup.c
index ec29d6b810..bf086dc5f9 100644
--- a/block/backup.c
+++ b/block/backup.c
@@ -336,6 +336,13 @@ static bool backup_cancel(Job *job, bool force)
     return true;
 }
 
+static bool backup_change(Job *job, JobChangeOptions *opts, Error **errp)
+{
+    BlockJob *bjob = container_of(job, BlockJob, job);
+
+    return block_job_change(bjob, &opts->u.backup, errp);
+}
+
 static const BlockJobDriver backup_job_driver = {
     .job_driver = {
         .instance_size          = sizeof(BackupBlockJob),
@@ -348,6 +355,7 @@ static const BlockJobDriver backup_job_driver = {
         .clean                  = backup_clean,
         .pause                  = backup_pause,
         .cancel                 = backup_cancel,
+        .change                 = backup_change,
     },
     .set_speed = backup_set_speed,
 };
diff --git a/block/commit.c b/block/commit.c
index 7c3fdcb0ca..ccb6097679 100644
--- a/block/commit.c
+++ b/block/commit.c
@@ -204,6 +204,13 @@ static int coroutine_fn commit_run(Job *job, Error **errp)
     return 0;
 }
 
+static bool commit_change(Job *job, JobChangeOptions *opts, Error **errp)
+{
+    BlockJob *bjob = container_of(job, BlockJob, job);
+
+    return block_job_change(bjob, &opts->u.commit, errp);
+}
+
 static const BlockJobDriver commit_job_driver = {
     .job_driver = {
         .instance_size = sizeof(CommitBlockJob),
@@ -213,7 +220,8 @@ static const BlockJobDriver commit_job_driver = {
         .run           = commit_run,
         .prepare       = commit_prepare,
         .abort         = commit_abort,
-        .clean         = commit_clean
+        .clean         = commit_clean,
+        .change        = commit_change,
     },
 };
 
diff --git a/block/mirror.c b/block/mirror.c
index e670d0dd4f..f474b87079 100644
--- a/block/mirror.c
+++ b/block/mirror.c
@@ -1251,41 +1251,45 @@ static bool commit_active_cancel(Job *job, bool force)
     return force || !job_is_ready(job);
 }
 
-static void mirror_change(Job *job, JobChangeOptions *opts, Error **errp)
+static bool mirror_change(Job *job, JobChangeOptions *opts, Error **errp)
 {
+    BlockJob *bjob = container_of(job, BlockJob, job);
     MirrorBlockJob *s = container_of(job, MirrorBlockJob, common.job);
     JobChangeOptionsMirror *change_opts = &opts->u.mirror;
-    MirrorCopyMode current;
-
-    /*
-     * The implementation relies on the fact that copy_mode is only written
-     * under the BQL. Otherwise, further synchronization would be required.
-     */
+    MirrorCopyMode old_mode;
 
     GLOBAL_STATE_CODE();
 
-    if (!change_opts->has_copy_mode) {
-        /* Nothing to do */
-        return;
-    }
+    if (change_opts->has_copy_mode) {
+        old_mode = qatomic_read(&s->copy_mode);
 
-    if (qatomic_read(&s->copy_mode) == change_opts->copy_mode) {
-        return;
-    }
+        if (old_mode != change_opts->copy_mode) {
+            if (change_opts->copy_mode != MIRROR_COPY_MODE_WRITE_BLOCKING) {
+                error_setg(errp, "Change to copy mode '%s' is not implemented",
+                           MirrorCopyMode_str(change_opts->copy_mode));
+                return false;
+            }
 
-    if (change_opts->copy_mode != MIRROR_COPY_MODE_WRITE_BLOCKING) {
-        error_setg(errp, "Change to copy mode '%s' is not implemented",
-                   MirrorCopyMode_str(change_opts->copy_mode));
-        return;
+            if (old_mode != MIRROR_COPY_MODE_BACKGROUND) {
+                error_setg(errp, "Expected current copy mode '%s', got '%s'",
+                           MirrorCopyMode_str(MIRROR_COPY_MODE_BACKGROUND),
+                           MirrorCopyMode_str(old_mode));
+                return false;
+            }
+        }
     }
 
-    current = qatomic_cmpxchg(&s->copy_mode, MIRROR_COPY_MODE_BACKGROUND,
-                              change_opts->copy_mode);
-    if (current != MIRROR_COPY_MODE_BACKGROUND) {
-        error_setg(errp, "Expected current copy mode '%s', got '%s'",
-                   MirrorCopyMode_str(MIRROR_COPY_MODE_BACKGROUND),
-                   MirrorCopyMode_str(current));
+    if (!block_job_change(bjob, qapi_JobChangeOptionsMirror_base(change_opts),
+                          errp))
+    {
+        return false;
     }
+
+    old_mode = qatomic_cmpxchg(&s->copy_mode, MIRROR_COPY_MODE_BACKGROUND,
+                               change_opts->copy_mode);
+    assert(old_mode == MIRROR_COPY_MODE_BACKGROUND);
+
+    return true;
 }
 
 static void mirror_query(BlockJob *job, BlockJobInfo *info)
@@ -1315,6 +1319,13 @@ static const BlockJobDriver mirror_job_driver = {
     .query                  = mirror_query,
 };
 
+static bool commit_active_change(Job *job, JobChangeOptions *opts, Error **errp)
+{
+    BlockJob *bjob = container_of(job, BlockJob, job);
+
+    return block_job_change(bjob, &opts->u.commit, errp);
+}
+
 static const BlockJobDriver commit_active_job_driver = {
     .job_driver = {
         .instance_size          = sizeof(MirrorBlockJob),
@@ -1327,6 +1338,7 @@ static const BlockJobDriver commit_active_job_driver = {
         .pause                  = mirror_pause,
         .complete               = mirror_complete,
         .cancel                 = commit_active_cancel,
+        .change                 = commit_active_change,
     },
     .drained_poll           = mirror_drained_poll,
 };
diff --git a/block/stream.c b/block/stream.c
index 7031eef12b..34f4588537 100644
--- a/block/stream.c
+++ b/block/stream.c
@@ -239,6 +239,13 @@ static int coroutine_fn stream_run(Job *job, Error **errp)
     return error;
 }
 
+static bool stream_change(Job *job, JobChangeOptions *opts, Error **errp)
+{
+    BlockJob *bjob = container_of(job, BlockJob, job);
+
+    return block_job_change(bjob, &opts->u.stream, errp);
+}
+
 static const BlockJobDriver stream_job_driver = {
     .job_driver = {
         .instance_size = sizeof(StreamBlockJob),
@@ -248,6 +255,7 @@ static const BlockJobDriver stream_job_driver = {
         .prepare       = stream_prepare,
         .clean         = stream_clean,
         .user_resume   = block_job_user_resume,
+        .change        = stream_change,
     },
 };
 
diff --git a/blockjob.c b/blockjob.c
index 2769722b37..d3cd4f4fbf 100644
--- a/blockjob.c
+++ b/blockjob.c
@@ -312,6 +312,19 @@ static bool block_job_set_speed(BlockJob *job, int64_t speed, Error **errp)
     return block_job_set_speed_locked(job, speed, errp);
 }
 
+bool block_job_change(BlockJob *job, JobChangeOptionsBlockJob *opts,
+                      Error **errp)
+{
+    GLOBAL_STATE_CODE();
+
+    if (!opts->has_speed) {
+        /* Nothing to do */
+        return true;
+    }
+
+    return block_job_set_speed(job, opts->speed, errp);
+}
+
 void block_job_ratelimit_processed_bytes(BlockJob *job, uint64_t n)
 {
     IO_CODE();
diff --git a/include/block/blockjob.h b/include/block/blockjob.h
index 72e849a140..fe433c8d35 100644
--- a/include/block/blockjob.h
+++ b/include/block/blockjob.h
@@ -224,4 +224,11 @@ bool block_job_is_internal(BlockJob *job);
  */
 const BlockJobDriver *block_job_driver(BlockJob *job);
 
+/**
+ * Common part of .change handler for block-jobs.
+ * Applies changes described in opts to the job.
+ */
+bool block_job_change(BlockJob *job, JobChangeOptionsBlockJob *opts,
+                      Error **errp);
+
 #endif
diff --git a/include/qemu/job.h b/include/qemu/job.h
index d44cdb0cc8..1c9da74a0c 100644
--- a/include/qemu/job.h
+++ b/include/qemu/job.h
@@ -313,7 +313,7 @@ struct JobDriver {
      *
      * Note that this can already be called before the job coroutine is running.
      */
-    void (*change)(Job *job, JobChangeOptions *opts, Error **errp);
+    bool (*change)(Job *job, JobChangeOptions *opts, Error **errp);
 
     /**
      * Called when the job is freed.
diff --git a/qapi/block-core.json b/qapi/block-core.json
index f5cefa441b..93f96e747e 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -3062,6 +3062,17 @@
 { 'command': 'block-job-finalize', 'data': { 'id': 'str' },
   'allow-preconfig': true }
 
+##
+# @JobChangeOptionsBlockJob:
+#
+# @speed: Change job speed (in bytes per second).  Supported for
+#     for backup, mirror, commit and stream jobs.
+#
+# Since: 9.1
+##
+{ 'struct': 'JobChangeOptionsBlockJob',
+  'data': { '*speed' : 'uint64' } }
+
 ##
 # @JobChangeOptionsMirror:
 #
@@ -3071,12 +3082,17 @@
 # Since: 8.2
 ##
 { 'struct': 'JobChangeOptionsMirror',
+  'base': 'JobChangeOptionsBlockJob',
   'data': { '*copy-mode' : 'MirrorCopyMode' } }
 
+
 ##
 # @JobChangeOptions:
 #
-# Block job options that can be changed after job creation.
+# Job options that can be changed after job creation. When option is
+# not specified the corresponding job parameter remains unchanged.
+# The change is transactional: on success all changes are applied
+# successfully, on failure nothing is changed.
 #
 # @id: The job identifier
 #
@@ -3093,7 +3109,10 @@
   'base': { 'id': 'str',
             '*type': { 'type': 'JobType', 'features': ['deprecated'] } },
   'discriminator': 'JobType',
-  'data': { 'mirror': 'JobChangeOptionsMirror' } }
+  'data': { 'mirror': 'JobChangeOptionsMirror',
+            'backup': 'JobChangeOptionsBlockJob',
+            'stream': 'JobChangeOptionsBlockJob',
+            'commit': 'JobChangeOptionsBlockJob' } }
 
 ##
 # @block-job-change:
