diff --git includes/common.inc includes/common.inc
index 5cea431..6852584 100644
--- includes/common.inc
+++ includes/common.inc
@@ -77,6 +77,64 @@ define('JS_THEME', 100);
 define('HTTP_REQUEST_TIMEOUT', 1);
 
 /**
+ * Constants defining cache granularity for blocks.
+ *
+ * Modules specify the caching patterns for their blocks using binary
+ * combinations of these constants in their hook_block_info():
+ *   $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. 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...).
+ * Blocks requiring more fine-grained clearing might consider disabling the
+ * built-in block cache (BLOCK_NO_CACHE) and roll their own.
+ *
+ * Note that user 1 is excluded from block caching.
+ */
+
+/**
+ * The block should not get cached. This setting should be used:
+ * - for simple blocks (notably those that do not perform any db query),
+ * where querying the db cache would be more expensive than directly generating
+ * the content.
+ * - for blocks that change too frequently.
+ */
+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.
+ */
+define('BLOCK_CACHE_PER_ROLE', 0x0001);
+
+/**
+ * The block can change depending on the user viewing the page.
+ * This setting can be resource-consuming for sites with large number of users,
+ * and thus should only be used when BLOCK_CACHE_PER_ROLE is not sufficient.
+ */
+define('BLOCK_CACHE_PER_USER', 0x0002);
+
+/**
+ * The block can change depending on the page being viewed.
+ */
+define('BLOCK_CACHE_PER_PAGE', 0x0004);
+
+/**
+ * The block is the same for every user on every page where it is visible.
+ */
+define('BLOCK_CACHE_GLOBAL', 0x0008);
+
+/**
  * Add content to a specified region.
  *
  * @param $region
@@ -3940,6 +3998,24 @@ function drupal_render_page($page) {
  * the placement of the form's children while not at all having to deal with
  * the form markup itself.
  *
+ * drupal_render() can optionally cache the rendered output of elements to
+ * improve performance. To use drupal_render() caching, set the element's #cache
+ * property to an associative array with one or several of the following keys:
+ *    - 'keys': An array of one or more keys that identify the element. If 'keys'
+ *       is set, the cache ID is created automatically from these keys.
+ *       @see drupal_render_cid_create()
+ *    - 'granularity' (optional): Define the cache granularity using binary
+ *       combinations of the cache granularity constants, e.g. BLOCK_CACHE_PER_USER
+ *       to cache for each user seperately or
+ *       BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE to cache seperately for each
+ *       page and role. If not specified the element is cached globally for each
+ *       theme and language.
+ *    - 'cid': Specify the cache ID directly. Either 'keys' or 'cid' is required.
+ *       If 'cid' is set, 'keys' and 'granularity' are ignored. Use only if you
+ *       have special requirements.
+ *    - 'expire': Set to one of the cache lifetime constants.
+ *    - 'bin': Specify a cache bin to cache the element in. Defaults to 'cache'.
+ *
  * This function is usually called from within another function, like
  * drupal_get_form() or a theme function. Elements are sorted internally
  * using uasort(). Since this is expensive, when passing already sorted
@@ -3963,6 +4039,11 @@ function drupal_render(&$elements) {
     return;
   }
 
+  // Try to fetch the element's markup from cache and return.
+  if ($cached_output = drupal_render_cache_get($elements)) {
+    return $cached_output;
+  }
+
   // If the default values for this element have not been loaded yet, populate
   // them.
   if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
@@ -4036,6 +4117,11 @@ function drupal_render(&$elements) {
   $prefix = isset($elements['#prefix']) ? $elements['#prefix'] : '';
   $suffix = isset($elements['#suffix']) ? $elements['#suffix'] : '';
 
+  // Cache the processed element if #cache is set.
+  if (isset($elements['#cache'])) {
+    drupal_render_cache_set($prefix . $elements['#children'] . $suffix, $elements);
+  }
+
   $elements['#printed'] = TRUE;
   return $prefix . $elements['#children'] . $suffix;
 }
@@ -4114,6 +4200,135 @@ function show(&$element) {
 }
 
 /**
+ * Get the rendered output of a renderable element from cache.
+ *
+ * @see drupal_render()
+ * @see drupal_render_cache_set()
+ * 
+ * @param $elements
+ *   A renderable array.
+ * @return
+ *   A markup string containing the rendered content of the element, or FALSE
+ *   if no cached copy of the element is available.
+ */
+function drupal_render_cache_get($elements) {
+  if (!$cid = drupal_render_cid_create($elements)) {
+    return FALSE;
+  }
+  $bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'cache';
+
+  if (!empty($cid) && $cache = cache_get($cid, $bin)) {
+    // Add additional libraries, CSS and JavaScript associated with this element.
+    drupal_process_attached(
+      isset($cache->data['#attached_library']) ? $cache->data['#attached_library'] : array(),
+      isset($cache->data['#attached_js']) ? $cache->data['#attached_js'] : array(),
+      isset($cache->data['#attached_css']) ? $cache->data['#attached_css'] : array()
+    );
+    // Return the rendered output.
+    return $cache->data['#markup'];;
+  }
+  return FALSE;
+}
+
+/**
+ * Cache the rendered output of a renderable element.
+ *
+ * This is called by drupal_render() if the #cache property is set on an element.
+ *
+ * @see drupal_render()
+ * @see drupal_render_cache_get()
+ *
+ * @param $markup
+ *   The rendered output string of $elements.
+ * @param $elements
+ *   A renderable array.
+ */
+function drupal_render_cache_set($markup, $elements) {
+  // Create the cache ID for the element 
+  if (!$cid = drupal_render_cid_create($elements)) {
+    return FALSE;
+  }
+
+  $data['#markup'] = $markup;
+  // Persist additional CSS and JavaScript files associated with this element.
+  foreach (array('css', 'js', 'library') 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($cid, $data, $bin, $expire);
+}
+
+/**
+ * Helper function for building cache ids.
+ *
+ * @param $granularity
+ *   One or more cache granularity constants, e.g. BLOCK_CACHE_PER_USER to cache
+ *   for each user seperately or BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE to
+ *   cache seperately for each page and role.
+ *
+ * @return
+ *   An array of cache ID parts, always containing the active theme. If the
+ *   locale module is enabled it also contains the active language. If
+ *   $granularity was passed in, more parts are added.
+ */
+function drupal_render_cid_parts($granularity = NULL) {
+  global $theme, $base_root, $user;
+
+  $cid_parts[] = $theme;
+  if (module_exists('locale')) {
+    global $language;
+    $cid_parts[] = $language->language;
+  }
+
+  if (!empty($granularity)) {
+    // '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
+    // equivocal, we favor the less expensive 'PER_ROLE' pattern.
+    if ($granularity & BLOCK_CACHE_PER_ROLE) {
+      $cid_parts[] = 'r.' . implode(',', array_keys($user->roles));
+    }
+    elseif ($granularity & BLOCK_CACHE_PER_USER) {
+      $cid_parts[] = "u.$user->uid";
+    }
+
+    if ($granularity & BLOCK_CACHE_PER_PAGE) {
+      $cid_parts[] = $base_root . request_uri();
+    }
+  }
+
+  return $cid_parts;
+}
+
+/**
+ * Create the cache ID for a renderable element.
+ *
+ * This creates the cache ID string, either by returning the #cache['cid']
+ * property if present or by building the cache ID out of the #cache['keys']
+ * and, optionally, the #cache['granularity'] properties.
+ *
+ * @param $elements
+ *   A renderable array.
+ *
+ * @return
+ *   The cache ID string, or FALSE if the element may not be cached.
+ */
+function drupal_render_cid_create($elements) {
+  if (isset($elements['#cache']['cid'])) {
+    return $elements['#cache']['cid'];
+  }
+  elseif (isset($elements['#cache']['keys'])) {
+    $granularity = isset($elements['#cache']['granularity']) ? $elements['#cache']['granularity'] : NULL;
+    // Merge in additional cache ID parts based provided by drupal_render_cid_parts().
+    $cid_parts = array_merge($elements['#cache']['keys'], drupal_render_cid_parts($granularity));
+    return implode(':', $cid_parts);
+  }
+  return FALSE;
+}
+
+/**
  * 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 509f6dd..c003f63 100644
--- modules/block/block.module
+++ modules/block/block.module
@@ -12,55 +12,6 @@
 define('BLOCK_REGION_NONE', -1);
 
 /**
- * Constants defining cache granularity for blocks.
- *
- * Modules specify the caching patterns for their blocks using binary
- * combinations of these constants in their hook_block_info():
- *   $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.
- *
- * 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...).
- * Blocks requiring more fine-grained clearing might consider disabling the
- * built-in block cache (BLOCK_NO_CACHE) and roll their own.
- *
- * Note that user 1 is excluded from block caching.
- */
-
-/**
- * The block should not get cached. This setting should be used:
- * - for simple blocks (notably those that do not perform any db query),
- * where querying the db cache would be more expensive than directly generating
- * the content.
- * - for blocks that change too frequently.
- */
-define('BLOCK_NO_CACHE', -1);
-
-/**
- * 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.
- */
-define('BLOCK_CACHE_PER_ROLE', 0x0001);
-
-/**
- * The block can change depending on the user viewing the page.
- * This setting can be resource-consuming for sites with large number of users,
- * and thus should only be used when BLOCK_CACHE_PER_ROLE is not sufficient.
- */
-define('BLOCK_CACHE_PER_USER', 0x0002);
-
-/**
- * The block can change depending on the page being viewed.
- */
-define('BLOCK_CACHE_PER_PAGE', 0x0004);
-
-/**
- * The block is the same for every user on every page where it is visible.
- */
-define('BLOCK_CACHE_GLOBAL', 0x0008);
-
-/**
  * Implement hook_help().
  */
 function block_help($path, $arg) {
@@ -707,7 +658,7 @@ function block_block_info_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,37 +723,15 @@ function _block_render_blocks($region_blocks) {
  *   The string used as cache_id for the block.
  */
 function _block_get_cache_id($block) {
-  global $theme, $base_root, $user;
-
   // User 1 being out of the regular 'roles define permissions' schema,
   // 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) {
-    $cid_parts = array();
-
+  if (variable_get('block_cache', 0) && !in_array($block->cache, array(BLOCK_NO_CACHE, BLOCK_CACHE_CUSTOM)) && $user->uid != 1) {
     // 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;
-    }
-
-    // '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
-    // equivocal, we favor the less expensive 'PER_ROLE' pattern.
-    if ($block->cache & BLOCK_CACHE_PER_ROLE) {
-      $cid_parts[] = 'r.' . implode(',', array_keys($user->roles));
-    }
-    elseif ($block->cache & BLOCK_CACHE_PER_USER) {
-      $cid_parts[] = "u.$user->uid";
-    }
-
-    if ($block->cache & BLOCK_CACHE_PER_PAGE) {
-      $cid_parts[] = $base_root . request_uri();
-    }
+    $cid_parts += drupal_render_cid_parts($block->cache);
 
     return implode(':', $cid_parts);
   }
diff --git modules/forum/forum.module modules/forum/forum.module
index 0767ee7..10a2903 100644
--- modules/forum/forum.module
+++ modules/forum/forum.module
@@ -479,8 +479,14 @@ function forum_form_alter(&$form, $form_state, $form_id) {
  * Implement hook_block_info().
  */
 function forum_block_info() {
-  $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;
 }
 
@@ -506,41 +512,65 @@ 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('forum', 'f', 'f.vid = n.vid');
-    $query->join('taxonomy_term_data', 'td', 'td.tid = f.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;
+  $query = db_select('node', 'n');
+  $query->join('forum', 'f', 'f.vid = n.vid');
+  $query->join('taxonomy_term_data', 'td', 'td.tid = f.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;
-    }
+    case 'new':
+      $title = t('New forum topics');
+      $query
+        ->orderBy('n.nid', 'DESC')
+        ->range(0, variable_get('forum_block_num_new', '5'));
+      break;
+  }
 
-    $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;
-    }
+   $cache_keys[] = 'forum';
+   $cache_keys[] = $delta;
+   // selectQuery class is not yet serializable. This is a robust workaround.
+   $cache_keys[] = md5(serialize(array((string) $query, $query->getArguments())));
+
+   $block['subject'] = $title;
+   $block['content'] = array(
+     '#access' => user_access('access content'),
+     '#pre_render' => array('forum_block_view_pre_render'),
+     '#cache' => array(
+        'keys' => $cache_keys,
+        'expire' => CACHE_TEMPORARY,
+     ),
+     '#query' => $query,
+   );
+   return $block;
+}
+
+/**
+* A #pre_render callback. Lists nodes based on the element's #query property.
+*
+* @see forum_block_view().
+*
+* @return
+*   A renderable array.
+*/
+function forum_block_view_pre_render($elements) {
+  $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.')));
   }
+  return $elements;
 }
 
 /**
