diff --git includes/common.inc includes/common.inc
index 0e275fd..fc22be3 100644
--- includes/common.inc
+++ includes/common.inc
@@ -3664,7 +3664,7 @@ function drupal_render(&$elements) {
   }
 
   // Add additional CSS and JavaScript files associated with this element.
-  foreach (array('css', 'js') as $kind) {
+  foreach (drupal_attached_element_types() as $kind) {
     if (!empty($elements['#attached_' . $kind]) && is_array($elements['#attached_' . $kind])) {
       foreach ($elements['#attached_' . $kind] as $data => $options) {
         // If the value is not an array, it's a filename and passed as first
@@ -3798,6 +3798,86 @@ function show(&$element) {
 }
 
 /**
+ * Helper for elements which use caching to avoid work during rendering.
+ * Typically called as a #pre_render callback.
+ *
+ * @see forum_block_view()
+ * 
+ * @param $elements
+ *   A render() array.
+ * @return
+ *   A render array() enriched with cache data or with a post_render() 
+ *   callback that will save to cache.
+ */
+function drupal_render_cache_get($elements) {
+  $bin = isset($elements['#cache_bin']) ? $elements['#cache_bin'] : 'cache';
+  if ($cache = cache_get($elements['#cache_cid'], $bin)) {
+    // Cache hit - set the markup based on cached data.
+    $elements['#markup'] = $cache->data['#markup'];
+    $elements['#theme'] = 'markup';
+    foreach (drupal_attached_element_types() as $kind) {
+      if (!empty($cache->data['#attached_' . $kind])) {
+        $elements['#attached_' . $kind] = $cache->data['#attached_' . $kind];
+      }
+    }
+  } 
+  else {
+    // Cache miss. Let rendering happen normally; then cache it.
+    $elements['#post_render'][] = 'drupal_render_cache_set';
+  }
+  return $elements;
+}
+
+/**
+ * Cache the rendered HTML. Typically called as a #post_render callback.
+ *
+ * @param $children
+ *   Rendered HTML.
+ * @param $elements
+ *   A render() array.
+ * @return
+ *   $children, unmodified.
+ */
+function drupal_render_cache_set($children, $elements) {
+  $data['#markup'] = $children;
+  // Persist additional CSS and JavaScript files associated with this element.
+  foreach (drupal_attached_element_types() as $kind) {
+    if (!empty($elements['#attached_' . $kind])) {
+      $data['#attached_' . $kind] = $elements['#attached_' . $kind];
+    }
+  }
+  $bin = isset($elements['#cache_bin']) ? $elements['#cache_bin'] : 'cache';
+  $expire = isset($elements['#cache_expire']) ? $elements['#cache_expire'] : CACHE_PERMANENT;
+  cache_set($elements['#cache_cid'], $data, $bin, $expire);
+  return $children;
+}
+
+/**
+ * A list of object types that use the #attached feature of drupal_render().
+ */
+function drupal_attached_element_types() {
+  return array('css', 'js');
+}
+
+/**
+ * Helper function for building cache ids.
+ * 
+ * @see forum_block_view()
+ *
+ * @return void
+ */
+function drupal_render_cid_parts() {
+  global $theme;
+  
+  $cid_parts[] = $theme;
+  if (module_exists('locale')) {
+    global $language;
+    $cid_parts[] = $language->language;
+  }
+  return $cid_parts;
+}
+
+/**
  * Function used by uasort to sort structured arrays by weight.
  */
 function element_sort($a, $b) {
diff --git modules/block/block.module modules/block/block.module
index 0715c7f..b788643 100644
--- modules/block/block.module
+++ modules/block/block.module
@@ -18,7 +18,8 @@ define('BLOCK_REGION_NONE', -1);
  * combinations of these constants in their hook_block_list():
  *   $block[delta]['cache'] = BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE;
  * BLOCK_CACHE_PER_ROLE is used as a default when no caching pattern is
- * specified.
+ * specified. Use BLOCK_CACHE_CUSTOM to disable standard block cache and 
+ * implement 
  *
  * The block cache is cleared in cache_clear_all(), and uses the same clearing
  * policy than page cache (node, comment, user, taxonomy added or updated...).
@@ -38,6 +39,14 @@ define('BLOCK_REGION_NONE', -1);
 define('BLOCK_NO_CACHE', -1);
 
 /**
+ * The block is handling its own caching in its hook_block_view(). From the 
+ * perspective of the block cache system, this is equivalent to BLOCK_NO_CACHE.
+ * Useful when time based expiration is needed or a site uses a node access 
+ * which invalidates standard block cache.
+ */
+define('BLOCK_CACHE_CUSTOM', -2);
+
+/**
  * The block can change depending on the roles the user viewing the page belongs to.
  * This is the default setting, used when the block does not specify anything.
  */
@@ -703,7 +712,7 @@ function block_block_list_alter(&$blocks) {
  *   An array of block objects such as returned for one region by _block_load_blocks().
  *
  * @return
- *   An array of visible blocks with subject and content rendered.
+ *   An array of visible blocks as expected by drupal_render().
  */
 function _block_render_blocks($region_blocks) {
   foreach ($region_blocks as $key => $block) {
@@ -772,17 +781,13 @@ function _block_get_cache_id($block) {
   // it brings too many chances of having unwanted output get in the cache
   // and later be served to other users. We therefore exclude user 1 from
   // block caching.
-  if (variable_get('block_cache', 0) && $block->cache != BLOCK_NO_CACHE && $user->uid != 1) {
+  if (variable_get('block_cache', 0) && !in_array($block->cache, array(BLOCK_NO_CACHE, BLOCK_CACHE_CUSTOM)) && $user->uid != 1) {
     $cid_parts = array();
 
     // Start with common sub-patterns: block identification, theme, language.
     $cid_parts[] = $block->module;
     $cid_parts[] = $block->delta;
-    $cid_parts[] = $theme;
-    if (module_exists('locale')) {
-      global $language;
-      $cid_parts[] = $language->language;
-    }
+    $cid_parts += drupal_render_cid_parts();
 
     // 'PER_ROLE' and 'PER_USER' are mutually exclusive. 'PER_USER' can be a
     // resource drag for sites with many users, so when a module is being
diff --git modules/forum/forum.module modules/forum/forum.module
index 6abc95b..b11dc28 100644
--- modules/forum/forum.module
+++ modules/forum/forum.module
@@ -496,8 +496,14 @@ function forum_form_alter(&$form, $form_state, $form_id) {
  * Implement hook_block_list().
  */
 function forum_block_list() {
-  $blocks['active']['info'] = t('Active forum topics');
-  $blocks['new']['info'] = t('New forum topics');
+  $blocks['active'] = array(
+    'info' => t('Active forum topics'),
+    'cache' => BLOCK_CACHE_CUSTOM,
+  );
+  $blocks['new'] = array(
+    'info' => t('New forum topics'),
+    'cache' => BLOCK_CACHE_CUSTOM,
+  );
   return $blocks;
 }
 
@@ -523,41 +529,69 @@ function forum_block_save($delta = '', $edit = array()) {
  * most recently added forum topics.
  */
 function forum_block_view($delta = '') {
-  if (user_access('access content')) {
-    $query = db_select('node', 'n');
-    $query->join('taxonomy_term_node', 'tn', 'tn.vid = n.vid');
-    $query->join('taxonomy_term_data', 'td', 'td.tid = tn.tid');
-    $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
-    $query
-      ->fields('n', array('nid', 'title'))
-      ->fields('ncs', array('comment_count', 'last_comment_timestamp'))
-      ->condition('n.status', 1)
-      ->condition('td.vid', variable_get('forum_nav_vocabulary', 0))
-      ->addTag('node_access');
-    switch ($delta) {
-      case 'active':
-        $title = t('Active forum topics');
-        $query
-          ->orderBy('ncs.last_comment_timestamp', 'DESC')
-          ->range(0, variable_get('forum_block_num_active', '5'));
-        break;
-
-      case 'new':
-        $title = t('New forum topics');
-        $query
-          ->orderBy('n.nid', 'DESC')
-          ->range(0, variable_get('forum_block_num_new', '5'));
-        break;
-    }
+  $query = db_select('node', 'n');
+  $query->join('taxonomy_term_node', 'tn', 'tn.vid = n.vid');
+  $query->join('taxonomy_term_data', 'td', 'td.tid = tn.tid');
+  $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+  $query
+    ->fields('n', array('nid', 'title'))
+    ->fields('ncs', array('comment_count', 'last_comment_timestamp'))
+    ->condition('n.status', 1)
+    ->condition('td.vid', variable_get('forum_nav_vocabulary', 0))
+    ->addTag('node_access');
+  
+  switch ($delta) {
+    case 'active':
+      $title = t('Active forum topics');
+      $query
+        ->orderBy('ncs.last_comment_timestamp', 'DESC')
+        ->range(0, variable_get('forum_block_num_active', '5'));
+      break;
+    case 'new':
+      $title = t('New forum topics');
+      $query
+        ->orderBy('n.nid', 'DESC')
+        ->range(0, variable_get('forum_block_num_new', '5'));
+      break;
+  }
+  
+  $cid_parts = drupal_render_cid_parts();
+  // selectQuery class is not yet serializable. This is a robust workaround.
+  $cid_parts[] = md5(serialize(array((string)$query, $query->getArguments())));
+
+  $block['subject'] = $title;
+  $block['content'] = array(
+    '#access' => user_access('access content'),
+    '#pre_render' => array('drupal_render_cache_get', 'forum_block_view_pre_render'),
+    '#cache_cid' =>  "forum:$delta:" . implode(':', $cid_parts), 
+    '#cache_expire' => CACHE_TEMPORARY,
+    '#query' => $query,
+  );
+  return $block;
+}
 
-    $result = $query->execute();
-    $content = node_title_list($result);
-    if (!empty($content)) {
-      $block['subject'] = $title;
-      $block['content'] = $content . theme('more_link', url('forum'), t('Read the latest forum topics.'));
-      return $block;
+/**
+ * A #pre_render callback. Lists nodes or does nothing if we have a cache hit.
+ * 
+ * @see forum_block_view().
+ *
+ * @return 
+ *   A render() array.
+ */
+function forum_block_view_pre_render($elements) {
+  if (isset($elements['#markup'])) {
+    // Cache hit. Nothing to do.
+  }
+  else {
+    // Cache miss. Execute query and build render() array. 
+    $result = $elements['#query']->execute();
+    if ($node_title_list = node_title_list($result)) {
+      $elements['forum_list'] = array('#markup' => $node_title_list);
+      $elements['forum_more'] = array('#markup' => theme('more_link', url('forum'), t('Read the latest forum topics.')));
     }
   }
+    dsm($elements);
+  return $elements;
 }
 
 /**
