Cache max-age doesn't correctly expire at midnight I've got a node which cache should expire every day at midnight. Additionally I've a block showing the same node in another display mode, that also should expire every day at midnight. I thought this would be pretty easy by calculating max-age

page_cache is already disabled - dynamic_page_cache is enabled

.module file

function wt_lunch_preprocess_node(array &amp;$variables) {
  $node = $variables['node'];
  if ($node-&gt;bundle() == 'wt_lunch') {
    $maxAge = strtotime('tomorrow') - strtotime('now');
    $variables['#cache']['max-age'] = $maxAge;
    $today = strtolower(date('l'));
    $variables['max_age'] = $maxAge; // this line is just for debugging
  }
}


src/Plugin/Block/MyBlock.php

...
  public function build() {
    $maxAge = strtotime('tomorrow') - strtotime('now');
    $emptyResult = [
      '#markup' =&gt; '',
      '#cache' =&gt; ['max-age' =&gt; $maxAge]
    ];

    $node = \Drupal::entityTypeManager()
      -&gt;getStorage('node')
      -&gt;loadByProperties(['type' =&gt; 'wt_lunch']);

    if (!$node = reset($node)) {
      return $emptyResult;
    }

    $build = \Drupal::entityTypeManager()-&gt;getViewBuilder('node')-&gt;view($node, 'special');
    $build['#cache']['max-age'] = $maxAge;
    $build['#attached']['library'][] = 'wt_lunch/daily';
    $build['#attached']['drupalSettings']['wt']['lunch']['timer'] = $node-&gt;get('field_lunch_daily_timer')-&gt;value;
    $build['#attached']['drupalSettings']['wt']['lunch']['from'] = $node-&gt;get('field_lunch_daily_from')-&gt;value;
    $build['#attached']['drupalSettings']['wt']['lunch']['until'] = $node-&gt;get('field_lunch_daily_until')-&gt;value;
    $build['#attributes']['class'][] = 'lunch--daily';
    return $build;
  }
...


This works almost every day. Almost. Every 10-15 days the cache does not expire properly and keeps rendering the block from yesterday.

Today the block didn't expire correctly again. I've previously added hidden debug comments in my Twig template, showing cache infos in the rendered HTML:

{{ date().timestamp }} ==> 1543194778 (roundabout 1am yesterday)
{{ max_age }} ==>  78422 (see $maxAge in code above, about 23 hours)

So I can verify that $build['#cache']['max-age'] definitly was correctly calculated, yet I did get the outdated block today at 11am.

Can anybody give me a clue why the max-age of the block sometimes does not work?
I believe your flaw is that if there is day where no one views a node of bundle wt_lunch your nodes max age will get out of alignment; thus, your block will also be displaying an outdated node. 

For this, don't use preprocess_node, use Job Scheduler or hook_cron instead. 

You might also want to utilize the above with Ultimate Cron.