diff --git a/includes/update.inc b/includes/update.inc
index f7a8fd6..f9086ce 100644
--- a/includes/update.inc
+++ b/includes/update.inc
@@ -124,16 +124,34 @@ function update_fix_d8_requirements() {
  * Perform one update and store the results for display on finished page.
  *
  * If an update function completes successfully, it should return a message
- * as a string indicating success, for example:
+ * as a string indicating success. For example:
  * @code
  * return t('New index added successfully.');
  * @endcode
  *
- * Alternatively, it may return nothing. In that case, no message
- * will be displayed at all.
+ * For more complicated update functions, an array of messages may be returned
+ * instead. For example:
+ * @code
+ * $messages = array();
+ * // Load a few user accounts that need to be updated.
+ * // ...
+ * foreach ($accounts as $account) {
+ *   // Perform the update.
+ *   // ...
+ *   $messages[] = t('Updated account information for user %name', array('%name' => $account->name));
+ * }
+ * return $messages;
+ * @endcode
+ *
+ * Alternatively, the update function may return nothing. In that case, no
+ * message will be displayed at all. Module authors should use their discretion
+ * in determining when to return messages and how many to return; in general,
+ * as few messages should be returned as possible, so as not to overwhelm the
+ * user with unnecessary text.
  *
- * If it fails for whatever reason, it should throw an instance of
- * DrupalUpdateException with an appropriate error message, for example:
+ * If the update function fails for whatever reason, it should throw an
+ * instance of DrupalUpdateException with an appropriate error message. For
+ * example:
  * @code
  * throw new DrupalUpdateException(t('Description of what went wrong'));
  * @endcode
@@ -146,7 +164,9 @@ function update_fix_d8_requirements() {
  * If an update function needs to be re-run as part of a batch process, it
  * should accept the $sandbox array by reference as its first parameter
  * and set the #finished property to the percentage completed that it is, as a
- * fraction of 1.
+ * fraction of 1. Each time the update function is called, any messages that it
+ * returns will be accumulated into a list, so that all messages associated
+ * with it can be displayed together at the end of the update process.
  *
  * @param $module
  *   The module whose update will be run.
@@ -173,8 +193,20 @@ function update_do_one($module, $number, $dependency_map, &$context) {
   $ret = array();
   if (function_exists($function)) {
     try {
-      $ret['results']['query'] = $function($context['sandbox']);
-      $ret['results']['success'] = TRUE;
+      // Update functions can return a single message, an array of messages, or
+      // nothing at all.
+      $messages = $function($context['sandbox']);
+      if ($messages) {
+        if (!is_array($messages)) {
+          $messages = array($messages);
+        }
+        foreach ($messages as $message) {
+          $ret[] = array(
+            'message' => $message,
+            'success' => TRUE,
+          );
+        }
+      }
     }
     // @TODO We may want to do different error handling for different
     // exception types, but for now we'll just log the exception and
@@ -185,7 +217,11 @@ function update_do_one($module, $number, $dependency_map, &$context) {
       require_once DRUPAL_ROOT . '/includes/errors.inc';
       $variables = _drupal_decode_exception($e);
       // The exception message is run through check_plain() by _drupal_decode_exception().
-      $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables));
+      $ret[] = array(
+        'message' => t('%type: !message in %function (line %line of %file).', $variables),
+        'success' => FALSE,
+      );
+      $abort = TRUE;
     }
   }
 
@@ -202,13 +238,13 @@ function update_do_one($module, $number, $dependency_map, &$context) {
   }
   $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret);
 
-  if (!empty($ret['#abort'])) {
+  if (!empty($abort)) {
     // Record this function in the list of updates that were aborted.
     $context['results']['#abort'][] = $function;
   }
 
   // Record the schema update if it was completed successfully.
-  if ($context['finished'] == 1 && empty($ret['#abort'])) {
+  if ($context['finished'] == 1 && empty($abort)) {
     drupal_set_installed_schema_version($module, $number);
   }
 
diff --git a/modules/system/system.api.php b/modules/system/system.api.php
index d21af6a..38fbe0c 100644
--- a/modules/system/system.api.php
+++ b/modules/system/system.api.php
@@ -3137,9 +3137,9 @@ function hook_install() {
  *   reason, it will throw a PDOException.
  *
  * @return
- *   Optionally update hooks may return a translated string that will be displayed
- *   to the user. If no message is returned, no message will be presented to the
- *   user.
+ *   Optionally update hooks may return a translated string, or an array of
+ *   translated strings, that will be displayed to the user. If no message is
+ *   returned, no message will be presented to the user.
  */
 function hook_update_N(&$sandbox) {
   // For non-multipass updates, the signature can simply be;
diff --git a/update.php b/update.php
index 105bc9d..12eba2a 100644
--- a/update.php
+++ b/update.php
@@ -177,40 +177,35 @@ function update_results_page() {
 
   $output .= theme('item_list', array('items' => $links));
 
-  // Output a list of queries executed.
+  // Output a list of update messages.
   if (!empty($_SESSION['update_results'])) {
     $all_messages = '';
     foreach ($_SESSION['update_results'] as $module => $updates) {
       if ($module != '#abort') {
         $module_has_message = FALSE;
-        $query_messages = '';
-        foreach ($updates as $number => $queries) {
+        $module_messages = '';
+        foreach ($updates as $number => $results) {
           $messages = array();
-          foreach ($queries as $query) {
-            // If there is no message for this update, don't show anything.
-            if (empty($query['query'])) {
-              continue;
-            }
-
-            if ($query['success']) {
-              $messages[] = '<li class="success">' . $query['query'] . '</li>';
+          foreach ($results as $result) {
+            if ($result['success']) {
+              $messages[] = '<li class="success">' . $result['message'] . '</li>';
             }
             else {
-              $messages[] = '<li class="failure"><strong>Failed:</strong> ' . $query['query'] . '</li>';
+              $messages[] = '<li class="failure"><strong>Failed:</strong> ' . $result['message'] . '</li>';
             }
           }
 
           if ($messages) {
             $module_has_message = TRUE;
-            $query_messages .= '<h4>Update #' . $number . "</h4>\n";
-            $query_messages .= '<ul>' . implode("\n", $messages) . "</ul>\n";
+            $module_messages .= '<h4>Update #' . $number . "</h4>\n";
+            $module_messages .= '<ul>' . implode("\n", $messages) . "</ul>\n";
           }
         }
 
-        // If there were any messages in the queries then prefix them with the
+        // If there were any messages for this module then prefix them with the
         // module name and add it to the global message list.
         if ($module_has_message) {
-          $all_messages .= '<h3>' . $module . " module</h3>\n" . $query_messages;
+          $all_messages .= '<h3>' . $module . " module</h3>\n" . $module_messages;
         }
       }
     }
