How to get cache metadata from nested render array when returning response?
Render the nodes in a render context, so that the metadata can bubble up:

  $storage = \Drupal::entityTypeManager()-&gt;getStorage('node');
  $builder = \Drupal::entityTypeManager()-&gt;getViewBuilder('node');
  $nodes = $storage-&gt;loadMultiple([1,2,3]);
  $build = $builder-&gt;viewMultiple($nodes, 'teaser');

  $render_context = new \Drupal\Core\Render\RenderContext();
  \Drupal::service('renderer')-&gt;executeInRenderContext($render_context, function() use (&amp;$build) {
    \Drupal::service('renderer')-&gt;render($build, TRUE);
  });

  $response = new CacheableResponse();
  if (!$render_context-&gt;isEmpty()) {
    $response-&gt;addCacheableDependency($render_context-&gt;pop());
  }


The rendering is not lost. The rendered nodes are in $build['#markup'] and you can add the html to the response if that is what you want to do:

$response-&gt;setContent($build['#markup']);




Edit:

Just seen in the related question on the right a similar topic. While the above code is good for demonstration purpose, because it shows how to get cache metadata from a nested render array, the simpler code in the answer to How to handle cache metadata in an AJAX response? is actually working for this nested build array too, because the dependencies of the build array are added after rendering and at this point the metadata has bubbled up to the top level.