Why won't this update in hook_update_n() run in batches correctly?
Why are you expecting it to work as a batch, it's doing exactly what you're asking it to do and processing all the importers in the foreach loop!  foreach ($importers as $importer)  It's not magic, it can't assume that's what you want to batch  :o)

Try this instead (obviously I haven't tested it, I've just typed it straight into the text editor here - but it's essentially your code so it should be fine);
    

/**
 * Fix importer mappings for file and image fields to use :uri convention.
 */
function feeds_update_7210(&amp;$sandbox) {
  // If this is the first pass through this update function then set some variables.
  if (!isset($sandbox['progress'])) {
    $sandbox['importers'] = feeds_importer_load_all(TRUE);
    $sandbox['progress'] = 0;
    $sandbox['max'] = count($sandbox['importers']);
  }

  $importer = array_pop($sandbox['importers']);
  $processor = $importer-&gt;processor;
  $config = $processor-&gt;getConfig();
  foreach ($config['mappings'] as $id =&gt; $mapping) {
    // Act on mappings that do not contain a colon.
    if (!strpos($mapping['target'], ':')) {
      // Get field data. Check if $info is empty to weed out non-field mappings like temporary targets.
      $info = field_info_field($mapping['target']);
      if (!empty($info)) {
        // Act on file or image fields.
        if (in_array($info['type'], array('file', 'image'))) {
          // Add ':uri' to fix the mapping.
          $config['mappings'][$id]['target'] = $mapping['target'] . ':uri';
        }
      }
    }
  }
  // Add the updated config and save the importer.
  $processor-&gt;addConfig(array('mappings' =&gt; $config['mappings']));
  $importer-&gt;save();

  $sandbox['progress']++;

  // Set the value for finished. If progress == max then finished will be 1, signifying we are done.
  $sandbox['#finished'] = ($sandbox['progress'] / $sandbox['max']);

  if (empty($sandbox['importers'])) {
    return t('@importers importers processed.', array('@importers' =&gt; $sandbox['max']));
  }
}


For hints about my alterations see the API documentation - note that the naming convention changes don't alter anything, I just made them for Drupal core consistency.  The most important thing to note is that all I've really done, is to remove the foreach loop so one importer gets used at a time.

NOTE: For clarification (as it's really not obvious) the percentage shown relates to the number of batch operations run - not the amount of passes through one batch operation.  See _batch_process():

// Total progress is the number of operations that have fully run plus the
// completion level of the current operation.
$current    = $total - $remaining + $finished;
$percentage = _batch_api_percentage($total, $current);


On a normal batch operation you'd pass in several separate operations like this;

$batch = array(
  'operations' =&gt; array(
    array('batch_example_one_process', array($options1, $options2)),
    array('batch_example_two_process', array($options3, $options4)),
  ),


And you'd see 50% completion only after batch_example_one_process has completed.  I guess you can see that your code is working correctly though, as it clearly only processes one processor per pass - as long as they all get processed, you can confirm that batch api is working correctly.