Index: src/main/java/org/gbif/occurrencestore/persistence/hbase/HBaseStore.java
===================================================================
--- src/main/java/org/gbif/occurrencestore/persistence/hbase/HBaseStore.java	(revision 1960)
+++ src/main/java/org/gbif/occurrencestore/persistence/hbase/HBaseStore.java	(working copy)
@@ -132,7 +132,7 @@
     }
   }
 
-  private Result getRow(T key, String columnName) {
+  public Result getRow(T key, String columnName) {
     checkNotNull(key, "key can't be null");
     checkNotNull(columnName, "columnName can't be null");
 
Index: src/main/java/org/gbif/occurrencestore/persistence/keygen/HBaseLockingKeyService.java
===================================================================
--- src/main/java/org/gbif/occurrencestore/persistence/keygen/HBaseLockingKeyService.java	(revision 1960)
+++ src/main/java/org/gbif/occurrencestore/persistence/keygen/HBaseLockingKeyService.java	(working copy)
@@ -9,6 +9,9 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.hadoop.thirdparty.guava.common.base.Preconditions;
 
 import com.google.common.collect.Maps;
 import com.google.inject.Inject;
@@ -33,6 +36,12 @@
   private static final long WAIT_BEFORE_RETRY_MS = 5000;
   private static final int WAIT_SKEW = 4000;
   private static final long STALE_LOCK_TIME = 5 * 60 * 1000;
+  
+  // The number of IDs to reserve at a time in batch
+  private static final int BATCHED_ID_SIZE = 1000;
+  // the next available key to allocate 
+  private int nextKey = 1; 
+  private int maxReservedKeyInclusive = 0;  // in a batch
 
   private final Meter reattempts =
     Metrics.newMeter(HBaseLockingKeyService.class, "reattempts", "reattempts", TimeUnit.SECONDS);
@@ -144,13 +153,7 @@
     // now we have map of every lookupKey to either ALLOCATED or ALLOCATING, and locks on all ALLOCATING
     if (foundKey == null) {
       // we need a new key from counter table
-      Long longKey =
-        counterTableStore.incrementColumnValue(HBaseTableConstants.COUNTER_ROW, HBaseTableConstants.COUNTER_COLUMN, 1);
-      if (longKey > Integer.MAX_VALUE) {
-        throw new IllegalStateException(
-          "The next available occurrence id is greater than what Integer can handle. This is fatal!");
-      }
-      key = longKey.intValue();
+      key = getNextKey();
     } else {
       key = foundKey;
     }
@@ -172,6 +175,40 @@
 
   }
 
+  /**
+   * Provides the next available key. Because throughput of an incrementColumnValue is limited by HBase to a few 
+   * thousand calls per second, this implementation reserves a batch of IDs at a time, and then allocates them to 
+   * the calling threads, until they are exhausted, when it will go and reserve another batch. Failure scenarios
+   * will therefore mean IDs go unused. This is expected to be a rare scenario and therefore acceptable.
+   * 
+   * @return the next key
+   */
+  private synchronized int getNextKey() {
+    // if we have exhausted our reserved keys, get a new batch of them
+    if (nextKey > maxReservedKeyInclusive) {
+      // get batch, supporting retries (e.g. checkAndPut failed)
+      boolean success = false;
+      do {
+        // get the current value
+        Result row = counterTableStore.getRow(HBaseTableConstants.COUNTER_ROW, HBaseTableConstants.COUNTER_COLUMN);
+        Long currentKeyAsLong = ResultReader.getLong(row, HBaseTableConstants.COUNTER_COLUMN_FAMILY, HBaseTableConstants.COUNTER_COLUMN, null);
+        if (currentKeyAsLong != null && currentKeyAsLong.longValue() > Integer.MAX_VALUE) {
+          throw new IllegalStateException("HBase is issuing keys larger than Integer can support");
+        }
+        
+        // reserve a batch
+        int reserveUntil = currentKeyAsLong == null ? BATCHED_ID_SIZE : currentKeyAsLong.intValue() + BATCHED_ID_SIZE;
+        success = counterTableStore.checkAndPut(HBaseTableConstants.COUNTER_ROW, HBaseTableConstants.COUNTER_COLUMN, 
+          Bytes.toBytes(reserveUntil), HBaseTableConstants.COUNTER_COLUMN, Bytes.toBytes(currentKeyAsLong.intValue()), null);
+        
+        // update our local counters.  HBase gives the current key, so our next is one greater than that
+        nextKey = currentKeyAsLong == null ? 1 : currentKeyAsLong.intValue() + 1;
+        maxReservedKeyInclusive = reserveUntil;
+      } while(!success);
+    }
+    return nextKey++;
+  }
+
   private void releaseLocks(Map<String, KeyStatus> statusMap) {
     for (Map.Entry<String, KeyStatus> entry : statusMap.entrySet()) {
       if (entry.getValue() == KeyStatus.ALLOCATING) {
