Menu Callback and CSRF
Routes that need CSRF tokens have the _csrf_token: 'TRUE' route option. That's how URLs pointing to those routes automatically get a CSRF token.

So we need to look at \Drupal\Core\Access\RouteProcessorCsrf:

public function processOutbound($route_name, Route $route, array &amp;$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
  if ($route-&gt;hasRequirement('_csrf_token')) {
    …
    if (!$bubbleable_metadata) {
      $parameters['token'] = $this-&gt;csrfToken-&gt;get($path);
    }
    else {
      // Generate a placeholder and a render array to replace it.
      $placeholder = hash('sha1', $path);
      $placeholder_render_array = [
        '#lazy_builder' =&gt; ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
      ];

      // Instead of setting an actual CSRF token as the query string, we set
      // the placeholder, which will be replaced at the very last moment. This
      // ensures links with CSRF tokens don't break cacheability.
      $parameters['token'] = $placeholder;
      $bubbleable_metadata-&gt;addAttachments(['placeholders' =&gt; [$placeholder =&gt; $placeholder_render_array]]);
    }
  }
}


So there we can see that if $bubbleable_metadata is NULL, then we generate the CSRF token right away. $bubbleable_metadata being NULL signals to the CSRF route processor that the URL is being generated in a context where no cacheability metadata and attachments can be bubbled (e.g. when generating a URL to be sent in an e-mail). The downside is that the HTML containing this URL will then be uncacheable (well, very poorly cacheable), because the CSRF token is depends on the session.

That's why in the other case, where $bubbleable_metadata is a BubbleableMetadata instance, that signals to the CSRF route processor that the URL is being generated in a context where cacheability metadata and attachments can be bubbled (e.g. when generating a URL to be rendered in some HTML). So there we generate a placeholder token that will be replaced with the CSRF token, and specify a "placeholder" attachment that will ensure that placeholder token is replaced with a CSRF token at the last possible moment. Consequently, the HTML that this URL ends up in is still perfectly cacheable, because the URL is cacheable.

Now that problem has been confirmed. If this happens in a preprocess function, as the following one, then you end up with $variables['url'] = GeneratedUrl(…). Twig ought to bubble the cacheability metadata and attachment metadata in that object, but it does NOT, in all versions of Drupal 8 before 8.1.2. We're fixing this for Drupal 8.1.2 at https://www.drupal.org/node/2575519.

function hook_preprocess_HOOK(&amp;$variables) {
  $variables['url'] = \Drupal::url('mymodule.test');
}