diff --git a/firmware/buflib.c b/firmware/buflib.c
index 43fc4bd..e053b05 100644
--- a/firmware/buflib.c
+++ b/firmware/buflib.c
@@ -238,11 +238,23 @@ buflib_compact(struct buflib_context *ctx)
 {
     BDEBUGF("%s(): Compacting!\n", __func__);
     union buflib_data *block,
-                      *first_free = find_first_free(ctx);
+                      *hole = NULL;
     int shift = 0, len;
     /* Store the results of attempting to shrink the handle table */
     bool ret = handle_table_shrink(ctx);
-    for(block = first_free; block < ctx->alloc_end; block += len)
+    /* compaction has basically two modes of operation:
+     *  1) the buffer is nicely movable: In this mode, blocks can be simply
+     * moved towards the beginning. Free blocks add to a shift value,
+     * which is the amount to move.
+     *  2) the buffer contains unmovable blocks: unmovable blocks create
+     * holes and reset shift. Once a hole is found, we're trying to fill
+     * holes first, moving by shift is the fallback. As the shift is reset,
+     * this effectively splits the buffer into portions of movable blocks.
+     * This mode cannot be used if no holes are found yet as it only works
+     * when it moves blocks across the portions. On the other side,
+     * moving by shift only works within the same portion
+     * For simplicity only 1 hole at a time is considered */
+    for(block = find_first_free(ctx); block < ctx->alloc_end; block += len)
     {
         len = block->val;
         /* This block is free, add its length to the shift value */
@@ -253,22 +265,24 @@ buflib_compact(struct buflib_context *ctx)
             continue;
         }
         /* attempt to fill any hole */
-        if (-first_free->val >= block->val)
+        if (hole && -hole->val >= len)
         {
-            intptr_t size = -first_free->val;
-            union buflib_data* next_block = block + block->val;
-            if (move_block(ctx, block, first_free - block))
+            intptr_t hlen = -hole->val;
+            if (move_block(ctx, block, hole - block))
             {
-                /* moving was successful. Move alloc_end down if necessary */
-                if (ctx->alloc_end == next_block)
-                    ctx->alloc_end = block;
-                /* Mark the block behind the just moved as free
-                 * be careful to not overwrite an existing block */
-                if (size != block->val)
+                /* Move was successful. The memory at block is now free */
+                block->val = -len;
+                /* add its length to shift */
+                shift -= len;
+                /* Reduce the size of the hole accordingly
+                 * but be careful to not overwrite an existing block */
+                if (hlen != len)
                 {
-                    first_free += block->val;
-                    first_free->val = block->val - size; /* negative */
+                    hole += len;
+                    hole->val = len - hlen; /* negative */
                 }
+                else /* hole closed */
+                    hole = NULL;
                 continue;
             }
         }
@@ -281,6 +295,7 @@ buflib_compact(struct buflib_context *ctx)
             if (!move_block(ctx, block, shift))
             {
                 target_block->val = shift; /* this is a hole */
+                hole = target_block;
                 shift = 0;
             }
             else
@@ -289,6 +304,7 @@ buflib_compact(struct buflib_context *ctx)
                 union buflib_data* new_free = target_block + target_block->val;
                 new_free->val = shift;
             }
+            continue; /* block moved or unmovable: next! */
         }
     }
     /* Move the end-of-allocation mark, and return true if any new space has
