diff --git a/src/EntityHelperTrait.php b/src/EntityHelperTrait.php
index 908502a..46982ea 100644
--- a/src/EntityHelperTrait.php
+++ b/src/EntityHelperTrait.php
@@ -159,8 +159,8 @@ trait EntityHelperTrait {
    *   (optional) Array of context values, corresponding to the attributes on
    *   the embed HTML tag.
    *
-   * @return string
-   *   The HTML of the entity rendered with the Entity Embed Display plugin.
+   * @return array
+   *   A render array from entity_view().
    */
   protected function renderEntityEmbed(EntityInterface $entity, array $context = array()) {
     // Support the deprecated view-mode data attribute.
@@ -191,8 +191,8 @@ trait EntityHelperTrait {
     // Allow modules to alter the entity prior to embed rendering.
     $this->moduleHandler()->alter(array("{$context['data-entity-type']}_embed_context", 'entity_embed_context'), $context, $entity);
 
-    // Build and render the Entity Embed Display plugin, allowing modules to
-    // alter the result before rendering.
+    // Build the display plugin, allowing modules to alter the result before
+    // rendering.
     $build = $this->renderEntityEmbedDisplayPlugin(
       $entity,
       $context['data-entity-embed-display'],
@@ -220,9 +220,7 @@ trait EntityHelperTrait {
 
     // @todo Should this hook get invoked if $build is an empty array?
     $this->moduleHandler()->alter(array("{$context['data-entity-type']}_embed", 'entity_embed'), $build, $entity, $context);
-    $entity_output = $this->renderer()->render($build);
-
-    return $entity_output;
+    return $build;
   }
 
   /**
diff --git a/src/Plugin/Filter/EntityEmbedFilter.php b/src/Plugin/Filter/EntityEmbedFilter.php
index 01b4d73..70bc002 100644
--- a/src/Plugin/Filter/EntityEmbedFilter.php
+++ b/src/Plugin/Filter/EntityEmbedFilter.php
@@ -12,6 +12,8 @@ use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
 use Drupal\entity_embed\EntityHelperTrait;
 use Drupal\entity_embed\Exception\EntityNotFoundException;
 use Drupal\entity_embed\Exception\RecursiveRenderingException;
@@ -102,13 +104,24 @@ class EntityEmbedFilter extends FilterBase implements ContainerFactoryPluginInte
             }
 
             $access = $entity->access('view', NULL, TRUE);
-            $access_metadata = CacheableMetadata::createFromObject($access);
-            $entity_metadata = CacheableMetadata::createFromObject($entity);
-            $result = $result->merge($entity_metadata)->merge($access_metadata);
+            $result->addCacheableDependency($access);
 
             $context = $this->getNodeAttributesAsArray($node);
             $context += array('data-langcode' => $langcode);
-            $entity_output = $this->renderEntityEmbed($entity, $context);
+            $build = $this->renderEntityEmbed($entity, $context);
+            // We need to render the embedded entity:
+            // - without replacing placeholders, so that the placeholders are
+            //   only replaced at the last possible moment. Hence we cannot use
+            //   either renderPlain() or renderRoot(), so we must use render().
+            // - without bubbling beyond this filter, because filters must
+            //   ensure that the bubbleable metadata for the changes they make
+            //   when filtering text makes it onto the FilterProcessResult
+            //   object that they return ($result). To prevent that bubbling, we
+            //   must wrap the call to render() in a render context.
+            $entity_output = $this->renderer()->executeInRenderContext(new RenderContext(), function () use (&$build) {
+              return $this->renderer()->render($build);
+            });
+            $result = $result->merge(BubbleableMetadata::createFromRenderArray($build));
 
             $depth--;
           }
