diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index b7b274045f..1bbaf0881d 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -29,7 +29,7 @@
  *   ),
  *   bundle_label = @Translation("Media type"),
  *   handlers = {
- *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
+ *     "storage" = "Drupal\media\MediaStorage",
  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
  *     "list_builder" = "Drupal\media\MediaListBuilder",
  *     "access" = "Drupal\media\MediaAccessControlHandler",
@@ -374,15 +374,22 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco
   }
 
   /**
-   * {@inheritdoc}
+   * Sets the media entity's field values from the source's metadata.
+   *
+   * Fetching the metadata could be slow (e.g., if requesting it from a remote
+   * API), so this is called by \Drupal\media\MediaStorage::save() prior to it
+   * beginning the database transaction, whereas static::preSave() executes
+   * after the transaction has already started.
+   *
+   * @internal
+   *   Expose this as an API in
+   *   https://www.drupal.org/project/drupal/issues/2992426.
    */
-  public function save() {
+  public function prepareSave() {
     // @todo If the source plugin talks to a remote API (e.g. oEmbed), this code
     // might be performing a fair number of HTTP requests. This is dangerously
     // brittle and should probably be handled by a queue, to avoid doing HTTP
-    // operations during entity save. As it is, doing this before calling
-    // parent::save() is a quick-fix to avoid doing HTTP requests in the middle
-    // of a database transaction (which begins once we call parent::save()). See
+    // operations during entity save. See
     // https://www.drupal.org/project/drupal/issues/2976875 for more.
 
     // In order for metadata to be mapped correctly, $this->original must be
@@ -419,7 +426,6 @@ public function save() {
         }
       }
     }
-    return parent::save();
   }
 
   /**
diff --git a/core/modules/media/src/MediaStorage.php b/core/modules/media/src/MediaStorage.php
new file mode 100644
index 0000000000..a2366625a1
--- /dev/null
+++ b/core/modules/media/src/MediaStorage.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Drupal\media;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+
+
+/**
+ * Defines the storage handler class for media.
+ *
+ * The default storage is overridden to handle metadata fetching outside of the
+ * database transaction.
+ */
+class MediaStorage extends SqlContentEntityStorage {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(EntityInterface $media) {
+    // For backwards compatibility, modules that override the Media entity
+    // class, are not required to implement the prepareSave() method.
+    // @todo For Drupal 8.7, consider throwing a deprecation notice if the
+    //   method doesn't exist. See
+    //   https://www.drupal.org/project/drupal/issues/2992426 for further
+    //   discussion.
+    if (method_exists($media, 'prepareSave')) {
+      $media->prepareSave();
+    }
+    return parent::save($media);
+  }
+
+}
