diff --git pathauto.admin.inc pathauto.admin.inc
index 2d68517..fe837eb 100644
--- pathauto.admin.inc
+++ pathauto.admin.inc
@@ -85,15 +85,6 @@ function pathauto_admin_settings() {
     '#description' => t('Maximum text length of any component in the alias (e.g., [title]). 100 is recommended. See <a href="@pathauto-help">Pathauto help</a> for details.', array('@pathauto-help' => url('admin/help/pathauto'))),
   );
 
-  $form['general']['pathauto_max_bulk_update'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Maximum number of objects to alias in a bulk update'),
-    '#size' => 4,
-    '#maxlength' => 4,
-    '#default_value' => variable_get('pathauto_max_bulk_update', 50),
-    '#description' => t('Maximum number of objects of a given type which should be aliased during a bulk update. The default is 50 and the recommended number depends on the speed of your server. If bulk updates "time out" or result in a "white screen" then reduce the number.'),
-  );
-
   $actions = array(
     t('Do nothing. Leave the old alias intact.'),
     t('Create a new alias. Leave the existing alias functioning.'),
@@ -174,10 +165,8 @@ function pathauto_admin_settings() {
 
   // Call the hook on all modules - an array of 'settings' objects is returned
   $all_settings = module_invoke_all('pathauto', 'settings');
-  $modulelist = '';
-  $indexcount = 0;
+  $modulelist = array();
   foreach ($all_settings as $settings) {
-    $items = '';
     $module = $settings->module;
     $modulelist[] = $module;
     $patterndescr = $settings->patterndescr;
@@ -256,22 +245,6 @@ function pathauto_admin_settings() {
       '#value' => $doc,
     );
 
-    // If the module supports bulk updates, offer the update action here
-    if ($settings->bulkname) {
-      $variable = 'pathauto_'. $module .'_bulkupdate';
-      if (variable_get($variable, FALSE)) {
-        variable_set($variable, FALSE);
-        $function = $module .'_pathauto_bulkupdate';
-        call_user_func($function);
-      }
-      $form[$module][$variable] = array(
-        '#type' => 'checkbox',
-        '#title' => $settings->bulkname,
-        '#default_value' => FALSE,
-        '#description' => $settings->bulkdescr,
-      );
-    }
-
     // If the module supports feeds, offer to generate aliases for them
     if ($supportsfeeds) {
       $variable = 'pathauto_'. $module .'_applytofeeds';
@@ -290,13 +263,6 @@ function pathauto_admin_settings() {
         '#description' => t('The text to use for aliases for RSS feeds. Examples are "0/feed" (used throughout Drupal core) and "feed" (used by some contributed Drupal modules, like Views).'),
       );
     }
-
-  }
-
-  if (isset($do_index_bulkupdate) && $do_index_bulkupdate) {
-    drupal_set_message(format_plural($indexcount,
-      'Bulk generation of index aliases completed, one alias generated.',
-      'Bulk generation of index aliases completed, @count aliases generated.'));
   }
 
   // Keep track of which modules currently support pathauto
@@ -390,6 +356,103 @@ function pathauto_admin_settings_validate($form, &$form_state) {
 }
 
 /**
+ * Form contructor for path alias bulk update form.
+ */
+function pathauto_admin_update() {
+  _pathauto_include();
+
+  $form['#tree'] = TRUE;
+
+  $modules = variable_get('pathauto_modulelist', array());
+  foreach ($modules as $module) {
+    $settings = module_invoke($module, 'pathauto', 'settings');
+    if (empty($settings) || empty($settings->bulkname)) {
+      continue;
+    }
+    $form['bulk_update'][$module] = array(
+      '#type' => 'checkbox',
+      '#title' => $settings->bulkname,
+      '#default_value' => FALSE,
+      '#description' => $settings->bulkdescr,
+    );
+  }
+
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
+
+  return $form;
+}
+
+/**
+ * Form submit handler for path alias bulk update form.
+ */
+function pathauto_admin_update_submit($form, &$form_state) {
+  // Disable pathauto verbose mode; backup current value to restore it later.
+  // In case a previous bulk update failed for any reason, the temporary
+  // variable will be set already.
+  $mode = variable_get('pathauto_batch_verbose', NULL);
+  if (!isset($mode)) {
+    variable_set('pathauto_batch_verbose', variable_get('pathauto_verbose', FALSE));
+    variable_set('pathauto_verbose', FALSE);
+  }
+
+  $path = drupal_get_path('module', 'pathauto');
+
+  $batch = array(
+    'title' => t('Bulk updating path aliases'),
+    'operations' => array(),
+    'finished' => 'pathauto_admin_update_batch_finished',
+    'file' => $path . '/pathauto.admin.inc',
+  );
+  foreach ($form_state['values']['bulk_update'] as $module => $value) {
+    if (!empty($value)) {
+      $batch['operations'][] = array('pathauto_admin_update_batch_process', array($module));
+    }
+  }
+  batch_set($batch);
+}
+
+/**
+ * Common batch processing callback for all operations.
+ *
+ * Required to load our include files.
+ *
+ * @todo Allow other modules to also load an include file?
+ */
+function pathauto_admin_update_batch_process($module, &$context) {
+  _pathauto_include();
+
+  if (!isset($context['results']['count'])) {
+    $context['results']['count'] = 0;
+  }
+
+  $function = $module . '_pathauto_batch_update';
+  $function($context);
+}
+
+/**
+ * Batch finished callback.
+ */
+function pathauto_admin_update_batch_finished($success, $results, $operations) {
+  // This finished callback also needs to be invoked in case nothing happened,
+  // so we can restore the verbose mode below.
+  if (isset($results['count'])) {
+    if ($success) {
+      $message = format_plural($results['count'], 'One alias generated.', '@count aliases generated.');
+      $type = 'success';
+    }
+    else {
+      $message = t('Finished with errors.');
+      $type = 'error';
+    }
+    drupal_set_message($message, $type);
+  }
+
+  // Restore verbose mode setting.
+  variable_set('pathauto_verbose', variable_get('pathauto_batch_verbose', FALSE));
+  variable_del('pathauto_batch_verbose');
+}
+
+/**
  * Menu callback; select certain alias types to delete.
  */
 function pathauto_admin_delete() {
@@ -475,8 +538,8 @@ function pathauto_admin_delete_submit($form, &$form_state) {
  */
 function _pathauto_warn_wysiwygs() {
   $wysiwygs = array(
-    'bueditor', 'fckeditor', 'htmlarea', 'htmlbox', 'tinymce', 'whizzywig',
-    'widgeditor', 'wymeditor', 'wysiwyg', 'xstandard', 'yui_editor',
+    'bueditor', 'ckeditor', 'fckeditor', 'htmlarea', 'htmlbox', 'tinymce', 'whizzywig',
+    'widgeditor', 'wymeditor', 'xstandard', 'yui_editor',
   );
 
   $wysiwyg_problem = FALSE;
diff --git pathauto.install pathauto.install
index 697a21d..836921c 100644
--- pathauto.install
+++ pathauto.install
@@ -108,3 +108,12 @@ function pathauto_update_7() {
   }
   return $ret;
 }
+
+/**
+ * Remove the maximum aliases for bulk updates variable (processing via batch now).
+ */
+function pathauto_update_6201() {
+  $ret = array();
+  variable_del('pathauto_max_bulk_update');
+  return $ret;
+}
diff --git pathauto.module pathauto.module
index 1dbf113..16032e3 100644
--- pathauto.module
+++ pathauto.module
@@ -62,6 +62,14 @@ function pathauto_menu() {
     'type' => MENU_LOCAL_TASK,
     'file' => 'pathauto.admin.inc',
   );
+  $items['admin/build/path/update_bulk'] = array(
+    'title' => 'Bulk update',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('pathauto_admin_update'),
+    'access arguments' => array('administer url aliases'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'pathauto.admin.inc',
+  );
 
   return $items;
 }
@@ -178,14 +186,17 @@ function pathauto_path_alias_types() {
   if (module_exists('blog')) {
     $objects['blog/'] = t('User blogs');
   }
-  if (module_exists('taxonomy')) {
-    $objects['taxonomy/'] = t('Vocabularies and terms');
+  if (module_exists('contact')) {
+    $objects['user/%/contact'] = t('User contact forms');
   }
   if (module_exists('taxonomy')) {
+    $objects['taxonomy/term/'] = t('Vocabularies and terms');
+  }
+  if (module_exists('tracker')) {
     $objects['user/%/track'] = t('User trackers');
   }
   if (module_exists('forum')) {
-    $objects['forum/%'] = t('Forums');
+    $objects['forum/'] = t('Forums');
   }
   return $objects;
 }
diff --git pathauto_node.inc pathauto_node.inc
index b18f436..fe4710e 100644
--- pathauto_node.inc
+++ pathauto_node.inc
@@ -20,7 +20,7 @@ function node_pathauto($op) {
       $settings['groupheader'] = t('Node path settings');
       $settings['patterndescr'] = t('Default path pattern (applies to all node types with blank patterns below)');
       $settings['patterndefault'] = t('content/[title-raw]');
-      $settings['bulkname'] = t('Bulk generate aliases for nodes that are not aliased');
+      $settings['bulkname'] = t('Content');
       $settings['bulkdescr'] = t('Generate aliases for all existing nodes which do not already have aliases.');
 
       $patterns = token_get_list('node');
@@ -62,63 +62,86 @@ function node_pathauto($op) {
 }
 
 /**
- * Generate aliases for all nodes without aliases.
+ * Batch processing callback; Generate aliases for nodes.
  */
-function node_pathauto_bulkupdate() {
-  // From all node types, only attempt to update those with patterns
-  $pattern_types = array();
-
-  // If there's a default pattern we assume all types might be updated.
-  if (trim(variable_get('pathauto_node_pattern', ''))) {
-    $pattern_types = array_keys(node_get_types('names'));
-  }
-  else {
-    // Check first for a node specific pattern...
-    $languages = array();
-    if (module_exists('locale')) {
-      $languages = array('' => t('Language neutral')) + locale_language_list('name');
+function node_pathauto_batch_update(&$context) {
+  // Prepare query.
+  if (!isset($context['sandbox']['query'])) {
+    // From all node types, only attempt to update those with patterns
+    $pattern_types = array();
+    // If there's a default pattern we assume all types might be updated.
+    if (trim(variable_get('pathauto_node_pattern', ''))) {
+      $pattern_types = array_keys(node_get_types('names'));
     }
-    foreach (array_keys(node_get_types('names')) as $type) {
-      if (trim(variable_get('pathauto_node_'. $type .'_pattern', ''))) {
-        $pattern_types[$type] = $type;
-        continue;
+    else {
+      // Check first for a node specific pattern...
+      $languages = array();
+      if (module_exists('locale')) {
+        $languages = array('' => t('Language neutral')) + locale_language_list('name');
       }
-      // ...then for a node-language pattern.
-      if (variable_get('language_content_type_'. $type, 0) && $languages) {
-        foreach ($languages as $lang_code => $lang_name) {
-          if (trim(variable_get('pathauto_node_'. $type .'_'. $lang_code .'_pattern', ''))) {
-            $pattern_types[$type] = $type;
-            continue 2;
+      foreach (array_keys(node_get_types('names')) as $type) {
+        if (trim(variable_get('pathauto_node_'. $type .'_pattern', ''))) {
+          $pattern_types[$type] = $type;
+          continue;
+        }
+        // ...then for a node-language pattern.
+        if (variable_get('language_content_type_'. $type, 0) && $languages) {
+          foreach ($languages as $lang_code => $lang_name) {
+            if (trim(variable_get('pathauto_node_'. $type .'_'. $lang_code .'_pattern', ''))) {
+              $pattern_types[$type] = $type;
+              continue 2;
+            }
           }
         }
       }
     }
+    if (!$pattern_types) {
+      $context['finished'] = 1;
+      return;
+    }
+
+    $context['sandbox']['query'] = "FROM {node} n LEFT JOIN {url_alias} alias ON CONCAT('node/', CAST(n.nid AS CHAR)) = alias.src WHERE alias.src IS NULL AND n.type IN (" . db_placeholders($pattern_types, 'varchar') . ')';
+    $context['sandbox']['query_args'] = $pattern_types;
+  }
+
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(nid) " . $context['sandbox']['query'], $context['sandbox']['query_args']));
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['items'] = array();
+  }
+
+  // Get the next stack of items to process.
+  if (empty($context['sandbox']['items'])) {
+    $query = "SELECT n.nid, alias.src, alias.dst " . $context['sandbox']['query'];
+    $result = db_query_range($query, $context['sandbox']['query_args'], $context['sandbox']['count'], 1000);
+    while ($row = db_fetch_object($result)) {
+      $context['sandbox']['items'][] = $row;
+    }
   }
 
-  $count = 0;
-  if (count($pattern_types)) {
-    $query = "SELECT n.nid, n.vid, n.type, n.title, n.uid, n.created, n.language, alias.src, alias.dst FROM {node} n LEFT JOIN {url_alias} alias ON CONCAT('node/', CAST(n.nid AS CHAR)) = alias.src WHERE alias.src IS NULL AND n.type IN (". db_placeholders($pattern_types, 'varchar') .')';
-    $result = db_query_range($query, $pattern_types, 0, variable_get('pathauto_max_bulk_update', 50));
+  // Process the next item in the stack.
+  if ($row = array_shift($context['sandbox']['items'])) {
+    $context['sandbox']['count']++;
 
-    $placeholders = array();
-    while ($node_ref = db_fetch_object($result)) {
-      $node = node_load($node_ref->nid, NULL, TRUE);
-      $node->src = $node_ref->src;
-      $node->dst = $node_ref->dst;
-      if (module_exists('taxonomy')) {
-        // Must populate the terms for the node here for the category
-        // placeholders to work
-        $node->taxonomy = array_keys(taxonomy_node_get_terms($node));
-      }
-      $placeholders = pathauto_get_placeholders('node', $node);
-      $src = "node/$node->nid";
-      if (pathauto_create_alias('node', 'bulkupdate', $placeholders, $src, $node->nid, $node->type, $node->language)) {
-        $count++;
-      }
+    $node = node_load($row->nid, NULL, TRUE);
+    $node->src = $row->src;
+    $node->dst = $row->dst;
+    // Must populate the terms for the node here for category placeholders.
+    if (module_exists('taxonomy')) {
+      $node->taxonomy = array_keys(taxonomy_node_get_terms($node));
+    }
+    $placeholders = pathauto_get_placeholders('node', $node);
+    $src = "node/$node->nid";
+    if (pathauto_create_alias('node', 'bulkupdate', $placeholders, $src, $node->nid, $node->type, $node->language)) {
+      $type = node_get_types('name', $node);
+      $context['message'] = t('Updated alias for @type %title', array('@type' => $type, '%title' => $node->title));
+      // Update total counter.
+      $context['results']['count']++;
     }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of nodes completed, one alias generated.',
-    'Bulk generation of nodes completed, @count aliases generated.'));
+  if ($context['sandbox']['total'] > 0) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
diff --git pathauto_taxonomy.inc pathauto_taxonomy.inc
index 6f7c6c6..45a951b 100644
--- pathauto_taxonomy.inc
+++ pathauto_taxonomy.inc
@@ -29,7 +29,7 @@ function taxonomy_pathauto($op) {
         }
       }
       $settings['supportsfeeds'] = '0/feed';
-      $settings['bulkname'] = t('Bulk generate aliases for terms that are not aliased');
+      $settings['bulkname'] = t('Vocabularies and terms');
       $settings['bulkdescr'] = t('Generate aliases for all existing terms which do not already have aliases.');
 
       $vocabularies = taxonomy_get_vocabularies();
@@ -51,47 +51,73 @@ function taxonomy_pathauto($op) {
 }
 
 /**
- * Generate aliases for all categories without aliases.
+ * Batch processing callback; Generate aliases for taxonomy terms.
  */
-function taxonomy_pathauto_bulkupdate() {
-  // From all node types, only attempt to update those with patterns
-  $pattern_vids = array();
-  foreach (taxonomy_get_vocabularies() as $vid => $info) {
-    $pattern = trim(variable_get('pathauto_taxonomy_'. $vid .'_pattern', ''));
-
-    // If it's not set, check the default
-    // TODO - if there's a default we shouldn't do this crazy where statement because all vocabs get aliases
-    // TODO - special casing to exclude the forum vid (and the images vid and...?)
-    if (empty($pattern)) {
-      $pattern = trim(variable_get('pathauto_taxonomy_pattern', ''));
-    }
-    if (!empty($pattern)) {
-      $pattern_vids[] = $vid;
-      if (empty($vid_where)) {
-        $vid_where = " AND (vid = '%s' ";
+function taxonomy_pathauto_batch_update(&$context) {
+  // Prepare query.
+  if (!isset($context['sandbox']['query'])) {
+    // From all node types, only attempt to update those with patterns
+    $pattern_vids = array();
+    foreach (taxonomy_get_vocabularies() as $vid => $info) {
+      $pattern = trim(variable_get('pathauto_taxonomy_'. $vid .'_pattern', ''));
+
+      // If it's not set, check the default
+      // TODO - if there's a default we shouldn't do this crazy where statement because all vocabs get aliases
+      // TODO - special casing to exclude the forum vid (and the images vid and...?)
+      if (empty($pattern)) {
+        $pattern = trim(variable_get('pathauto_taxonomy_pattern', ''));
       }
-      else {
-        $vid_where .= " OR vid = '%s'";
+      if (!empty($pattern)) {
+        $pattern_vids[] = $vid;
+        if (empty($vid_where)) {
+          $vid_where = " AND (vid = '%s' ";
+        }
+        else {
+          $vid_where .= " OR vid = '%s'";
+        }
       }
     }
+    $vid_where .= ')';
+
+    // Exclude the forums and join all the args into one array so they can be passed to db_query
+    $forum_vid[] = variable_get('forum_nav_vocabulary', '');
+    $query_args = array_merge($forum_vid, $pattern_vids);
+
+    $context['sandbox']['query'] = "FROM {term_data} LEFT JOIN {url_alias} ON CONCAT('taxonomy/term/', CAST(tid AS CHAR)) = src WHERE src IS NULL AND vid <> %d " . $vid_where;
+    $context['sandbox']['query_args'] = $query_args;
+  }
+
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(tid) " . $context['sandbox']['query'], $context['sandbox']['query_args']));
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['items'] = array();
+  }
+
+  // Get the next stack of items to process.
+  if (empty($context['sandbox']['items'])) {
+    $query = "SELECT tid " . $context['sandbox']['query'];
+    $result = db_query_range($query, $context['sandbox']['query_args'], $context['sandbox']['count'], 1000);
+    while ($uid = db_result($result)) {
+      $context['sandbox']['items'][] = $uid;
+    }
   }
-  $vid_where .= ')';
 
-  // Exclude the forums and join all the args into one array so they can be passed to db_query
-  $forum_vid[] = variable_get('forum_nav_vocabulary', '');
-  $query_args = array_merge($forum_vid, $pattern_vids);
-  $query = "SELECT tid, vid, name, src, dst FROM {term_data} LEFT JOIN {url_alias} ON CONCAT('taxonomy/term/', CAST(tid AS CHAR)) = src WHERE src IS NULL AND vid <> %d ". $vid_where;
-  $result = db_query_range($query, $query_args, 0, variable_get('pathauto_max_bulk_update', 50));
+  // Process the next item in the stack.
+  if ($tid = array_shift($context['sandbox']['items'])) {
+    $context['sandbox']['count']++;
 
-  $count = 0;
-  $placeholders = array();
-  while ($category = db_fetch_object($result)) {
-    $count += _taxonomy_pathauto_alias($category, 'bulkupdate');
+    $category = taxonomy_get_term($tid);
+    if (_taxonomy_pathauto_alias($category, 'bulkupdate')) {
+      $context['message'] = t('Updated alias for term %title', array('%title' => $category->name));
+      // Update total counter.
+      $context['results']['count']++;
+    }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of terms completed, one alias generated.',
-    'Bulk generation of terms completed, @count aliases generated.'));
+  if ($context['sandbox']['total'] > 0) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
 
 /**
@@ -143,7 +169,7 @@ function forum_pathauto($op) {
         }
       }
       $settings['supportsfeeds'] = '0/feed';
-      $settings['bulkname'] = t('Bulk generate forum paths');
+      $settings['bulkname'] = t('Forums');
       $settings['bulkdescr'] = t('Generate aliases for all existing forums and forum containers which do not already have aliases.');
       return (object) $settings;
     default:
@@ -152,20 +178,47 @@ function forum_pathauto($op) {
 }
 
 /**
- * Generate aliases for all forums and forum containers without aliases.
+ * Batch processing callback; Generate aliases for forums.
  */
-function forum_pathauto_bulkupdate() {
-  $forum_vid = variable_get('forum_nav_vocabulary', '');
-  $query = "SELECT tid, vid, name, src, dst FROM {term_data} LEFT JOIN {url_alias} ON CONCAT('forum/', CAST(tid AS CHAR)) = src WHERE vid = %d AND src IS NULL";
-  $result = db_query_range($query, $forum_vid, 0, variable_get('pathauto_max_bulk_update', 50));
+function forum_pathauto_batch_update(&$context) {
+  // Prepare query.
+  if (!isset($context['sandbox']['query'])) {
+    $forum_vid = variable_get('forum_nav_vocabulary', '');
 
-  $count = 0;
-  $placeholders = array();
-  while ($category = db_fetch_object($result)) {
-    $count = _taxonomy_pathauto_alias($category, 'bulkupdate') + $count;
+    $context['sandbox']['query'] = "FROM {term_data} LEFT JOIN {url_alias} ON CONCAT('forum/', CAST(tid AS CHAR)) = src WHERE vid = %d AND src IS NULL";
+    $context['sandbox']['query_args'] = array($forum_vid);
+  }
+
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(tid) " . $context['sandbox']['query'], $context['sandbox']['query_args']));
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['items'] = array();
+  }
+
+  // Get the next stack of items to process.
+  if (empty($context['sandbox']['items'])) {
+    $query = "SELECT tid " . $context['sandbox']['query'];
+    $result = db_query_range($query, $context['sandbox']['query_args'], $context['sandbox']['count'], 1000);
+    while ($tid = db_result($result)) {
+      $context['sandbox']['items'][] = $tid;
+    }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk update of forums and forum containers completed, one alias generated.',
-    'Bulk update of forums and forum containers completed, @count aliases generated.'));
+  // Process the next item in the stack.
+  if ($tid = array_shift($context['sandbox']['items'])) {
+    $context['sandbox']['count']++;
+
+    $category = taxonomy_get_term($tid);
+    if (_taxonomy_pathauto_alias($category, 'bulkupdate')) {
+      $context['message'] = t('Updated alias for forum %title', array('%title' => $category->name));
+      // Update total counter.
+      $context['results']['count']++;
+    }
+  }
+
+  if ($context['sandbox']['total'] > 0) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
+
diff --git pathauto_user.inc pathauto_user.inc
index cbc6194..89648a6 100644
--- pathauto_user.inc
+++ pathauto_user.inc
@@ -29,7 +29,7 @@ function user_pathauto($op) {
         }
       }
 
-      $settings['bulkname'] = t('Bulk generate aliases for users that are not aliased');
+      $settings['bulkname'] = t('Users');
       $settings['bulkdescr'] = t('Generate aliases for all existing user account pages which do not already have aliases.');
       return (object) $settings;
     default:
@@ -54,7 +54,7 @@ function blog_pathauto($op) {
         $settings['placeholders']['['. $pattern .']'] = $description;
       }
       $settings['supportsfeeds'] = 'feed';
-      $settings['bulkname'] = t('Bulk generate aliases for blogs that are not aliased');
+      $settings['bulkname'] = t('User blogs');
       $settings['bulkdescr'] = t('Generate aliases for all existing blog pages which do not already have aliases.');
       return (object) $settings;
     default:
@@ -79,7 +79,7 @@ function tracker_pathauto($op) {
         $settings['placeholders']['['. $pattern .']'] = $description;
       }
       $settings['supportsfeeds'] = 'feed';
-      $settings['bulkname'] = t('Bulk generate aliases for user-tracker paths that are not aliased');
+      $settings['bulkname'] = t('User trackers');
       $settings['bulkdescr'] = t('Generate aliases for all existing user-tracker pages which do not already have aliases.');
       return (object) $settings;
     default:
@@ -103,7 +103,7 @@ function contact_pathauto($op) {
       foreach ($patterns['user'] as $pattern => $description) {
         $settings['placeholders']['['. $pattern .']'] = $description;
       }
-      $settings['bulkname'] = t('Bulk generate aliases for user contact form paths that are not aliased');
+      $settings['bulkname'] = t('User contact forms');
       $settings['bulkdescr'] = t('Generate aliases for all existing user contact form pages which do not already have aliases.');
       return (object) $settings;
     default:
@@ -112,91 +112,160 @@ function contact_pathauto($op) {
 }
 
 /**
- * Bulk generate aliases for all users without aliases.
+ * Batch processing callback; Generate aliases for users.
  */
-function user_pathauto_bulkupdate() {
-  $query = "SELECT uid, name, src, dst FROM {users} LEFT JOIN {url_alias} ON CONCAT('user/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL";
-  $result = db_query_range($query, 0, variable_get('pathauto_max_bulk_update', 50));
-
-  $count = 0;
-  $placeholders = array();
-  while ($user = db_fetch_object($result)) {
-    $placeholders = pathauto_get_placeholders('user', $user);
-    $src = 'user/'. $user->uid;
-    if (pathauto_create_alias('user', 'bulkupdate', $placeholders, $src, $user->uid)) {
-      $count++;
+function user_pathauto_batch_update(&$context) {
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(uid) FROM {users} LEFT JOIN {url_alias} ON CONCAT('user/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL"));
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['items'] = array();
+  }
+
+  // Get the next stack of items to process.
+  if (empty($context['sandbox']['items'])) {
+    $query = "SELECT uid FROM {users} LEFT JOIN {url_alias} ON CONCAT('user/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL";
+    $result = db_query_range($query, $context['sandbox']['count'], 1000);
+    while ($uid = db_result($result)) {
+      $context['sandbox']['items'][] = $uid;
+    }
+  }
+
+  // Process the next item in the stack.
+  if ($uid = array_shift($context['sandbox']['items'])) {
+    $context['sandbox']['count']++;
+
+    $account = user_load(array('uid' => $uid));
+    $placeholders = pathauto_get_placeholders('user', $account);
+    $src = 'user/' . $account->uid;
+    if (pathauto_create_alias('user', 'bulkupdate', $placeholders, $src, $account->uid)) {
+      $context['message'] = t('Updated alias for user %name', array('%name' => $account->name));
+      // Update total counter.
+      $context['results']['count']++;
     }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of users completed, one alias generated.',
-    'Bulk generation of users completed, @count aliases generated.'));
+  if ($context['sandbox']['total'] > 0) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
 
 /**
- * Bulk generate aliases for all blogs without aliases.
+ * Batch processing callback; Generate aliases for user blogs.
  */
-function blog_pathauto_bulkupdate() {
-  $query = "SELECT uid, name, src, dst FROM {users} LEFT JOIN {url_alias} ON CONCAT('blog/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL";
-  $result = db_query_range($query, 0, variable_get('pathauto_max_bulk_update', 50));
-
-  $count = 0;
-  $placeholders = array();
-  while ($user = db_fetch_object($result)) {
-    $placeholders = pathauto_get_placeholders('user', $user);
-    $src = 'blog/'. $user->uid;
-    if (pathauto_create_alias('blog', 'bulkupdate', $placeholders, $src, $user->uid)) {
-      $count++;
+function blog_pathauto_batch_update(&$context) {
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(uid) FROM {users} LEFT JOIN {url_alias} ON CONCAT('blog/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL"));
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['items'] = array();
+  }
+
+  // Get the next stack of items to process.
+  if (empty($context['sandbox']['items'])) {
+    $query = "SELECT uid FROM {users} LEFT JOIN {url_alias} ON CONCAT('blog/', CAST(uid AS CHAR)) = src WHERE uid > 0 AND src IS NULL";
+    $result = db_query_range($query, $context['sandbox']['count'], 1000);
+    while ($uid = db_result($result)) {
+      $context['sandbox']['items'][] = $uid;
     }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of user blogs completed, one alias generated.',
-    'Bulk generation of user blogs completed, @count aliases generated.'));
+  // Process the next item in the stack.
+  if ($uid = array_shift($context['sandbox']['items'])) {
+    $context['sandbox']['count']++;
+
+    $account = user_load(array('uid' => $uid));
+    $placeholders = pathauto_get_placeholders('user', $account);
+    $src = 'blog/'. $account->uid;
+    if (pathauto_create_alias('blog', 'bulkupdate', $placeholders, $src, $account->uid)) {
+      $context['message'] = t('Updated alias for blog of user %name', array('%name' => $account->name));
+      // Update total counter.
+      $context['results']['count']++;
+    }
+  }
+
+  if ($context['sandbox']['total'] > 0) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
 
 /**
- * Bulk generate aliases for user trackers without aliases.
+ * Batch processing callback; Generate aliases for user tracker pages.
  */
-function tracker_pathauto_bulkupdate() {
-  // We do the double CONCAT because Pgsql8.1 doesn't support more than three arguments to CONCAT
-  // Hopefully some day we can remove that.
-  $query = "SELECT uid, name, src, dst FROM {users} LEFT JOIN {url_alias} ON CONCAT(CONCAT('user/', CAST(uid AS CHAR)), '/track') = src WHERE uid > 0 AND src IS NULL";
-  $result = db_query_range($query, 0, variable_get('pathauto_max_bulk_update', 50));
-
-  $count = 0;
-  $placeholders = array();
-  while ($user = db_fetch_object($result)) {
-    $placeholders = pathauto_get_placeholders('user', $user);
-    $src = 'user/'. $user->uid .'/track';
-    if (pathauto_create_alias('tracker', 'bulkupdate', $placeholders, $src, $user->uid)) {
-      $count++;
+function tracker_pathauto_batch_update(&$context) {
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(uid) FROM {users} LEFT JOIN {url_alias} ON CONCAT(CONCAT('user/', CAST(uid AS CHAR)), '/track') = src WHERE uid > 0 AND src IS NULL"));
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['items'] = array();
+  }
+
+  // Get the next stack of items to process.
+  if (empty($context['sandbox']['items'])) {
+    // We do the double CONCAT because Pgsql8.1 doesn't support more than three
+    // arguments to CONCAT.
+    $query = "SELECT uid FROM {users} LEFT JOIN {url_alias} ON CONCAT(CONCAT('user/', CAST(uid AS CHAR)), '/track') = src WHERE uid > 0 AND src IS NULL";
+    $result = db_query_range($query, $context['sandbox']['count'], 1000);
+    while ($uid = db_result($result)) {
+      $context['sandbox']['items'][] = $uid;
     }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of user tracker pages completed, one alias generated.',
-    'Bulk generation of user tracker pages completed, @count aliases generated.'));
+  // Process the next item in the stack.
+  if ($uid = array_shift($context['sandbox']['items'])) {
+    $context['sandbox']['count']++;
+
+    $account = user_load(array('uid' => $uid));
+    $placeholders = pathauto_get_placeholders('user', $account);
+    $src = 'user/' . $account->uid . '/track';
+    if (pathauto_create_alias('tracker', 'bulkupdate', $placeholders, $src, $account->uid)) {
+      $context['message'] = t('Updated alias for tracker of user %name', array('%name' => $account->name));
+      // Update total counter.
+      $context['results']['count']++;
+    }
+  }
+
+  if ($context['sandbox']['total'] > 0) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
 
 /**
- * Bulk generate aliases for all users without aliases
+ * Batch processing callback; Generate aliases for user contact forms.
  */
-function contact_pathauto_bulkupdate() {
-  $query = "SELECT uid, name, src, dst FROM {users} LEFT JOIN {url_alias} ON CONCAT(CONCAT('user/', CAST(uid AS CHAR)), '/contact') = src WHERE uid > 0 AND src IS NULL";
-  $result = db_query_range($query, 0, variable_get('pathauto_max_bulk_update', 50));
-
-  $count = 0;
-  $placeholders = array();
-  while ($user = db_fetch_object($result)) {
-    $placeholders = pathauto_get_placeholders('user', $user);
-    $src = 'user/'. $user->uid .'/contact';
-    if (pathauto_create_alias('contact', 'bulkupdate', $placeholders, $src, $user->uid)) {
-      $count++;
+function contact_pathauto_batch_update(&$context) {
+  // Get the total amount of items to process.
+  if (!isset($context['sandbox']['total'])) {
+    $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(uid) FROM {users} LEFT JOIN {url_alias} ON CONCAT(CONCAT('user/', CAST(uid AS CHAR)), '/contact') = src WHERE uid > 0 AND src IS NULL"));
+    $context['sandbox']['count'] = 0;
+    $context['sandbox']['items'] = array();
+  }
+
+  // Get the next stack of items to process.
+  if (empty($context['sandbox']['items'])) {
+    $query = "SELECT uid FROM {users} LEFT JOIN {url_alias} ON CONCAT(CONCAT('user/', CAST(uid AS CHAR)), '/contact') = src WHERE uid > 0 AND src IS NULL";
+    $result = db_query_range($query, $context['sandbox']['count'], 1000);
+    while ($uid = db_result($result)) {
+      $context['sandbox']['items'][] = $uid;
     }
   }
 
-  drupal_set_message(format_plural($count,
-    'Bulk generation of contact pages completed, one alias generated.',
-    'Bulk generation of contact pages completed, @count aliases generated.'));
+  // Process the next item in the stack.
+  if ($uid = array_shift($context['sandbox']['items'])) {
+    $context['sandbox']['count']++;
+
+    $account = user_load(array('uid' => $uid));
+    $placeholders = pathauto_get_placeholders('user', $account);
+    $src = 'user/' . $account->uid . '/contact';
+    if (pathauto_create_alias('contact', 'bulkupdate', $placeholders, $src, $account->uid)) {
+      $context['message'] = t('Updated alias for contact form of user %name', array('%name' => $account->name));
+      // Update total counter.
+      $context['results']['count']++;
+    }
+  }
+
+  if ($context['sandbox']['total'] > 0) {
+    $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total'];
+  }
 }
+
