diff --git a/gomen/pc-solver.html b/gomen/pc-solver.html
index dbcb851..6ed45e3 100644
--- a/gomen/pc-solver.html
+++ b/gomen/pc-solver.html
@@ -173,6 +173,8 @@ <h1>PC solver</h1>
             <label><input id="hold" type="checkbox" checked> Use hold</label>
         </div>
 
+        <progress id="progress" style="grid-column: span 2;"></progress>
+
     </div>
 
     <div id="loading">Loading solver...</div>
@@ -187,7 +189,9 @@ <h1>PC solver</h1>
         let queue = document.getElementById("queue");
         let queueErrors = document.getElementById("queue-errors");
         let hold = document.getElementById("hold");
+        let progress = document.getElementById("progress");
         let solutions = document.getElementById("solutions");
+        let progressTimeout = window.setTimeout(() => { }, 0);
 
         worker.onmessage = message => {
             if (message.data.kind == "ready") {
@@ -195,6 +199,8 @@ <h1>PC solver</h1>
 
                 if (work != null) {
                     worker.postMessage(work);
+                } else {
+                    hideProgress();
                 }
 
                 return;
@@ -205,20 +211,28 @@ <h1>PC solver</h1>
                 initialErrors.innerText = "inital field has no solutions or is unreachable";
                 solutions.innerHTML = "";
                 solutions.classList.remove("loading");
+                hideProgress();
                 return;
             } else if (message.data.kind == "possible" && message.data.query.garbage == getGarbage()[0]) {
                 initialErrors.innerText = "";
                 return;
             }
 
+            if (message.data.kind == "progress") {
+                progress.value = message.data.amount;
+                return;
+            }
+
             if (message.data.query.queue != work.queue
                 || message.data.query.garbage != work.garbage
                 || message.data.query.hold != work.hold
                 || message.data.query.count != work.count) {
                 worker.postMessage(work);
+                showProgress();
                 return;
             } else {
                 work = null;
+                hideProgress();
                 solutions.innerHTML = "";
                 solutions.classList.remove("loading");
             }
@@ -234,6 +248,18 @@ <h1>PC solver</h1>
             }
         }
 
+        function hideProgress() {
+            window.clearTimeout(progressTimeout);
+            progress.style.display = "none";
+        }
+        function showProgress() {
+            progress.removeAttribute("value");
+            window.clearTimeout(progressTimeout);
+            progressTimeout = window.setTimeout(() => {
+                progress.style.display = "";
+            }, 1000);
+        }
+
         function showSolutions(solns, count) {
             if (solns.length == 0) {
                 solutions.innerText = "no solutions";
@@ -310,6 +336,7 @@ <h1>PC solver</h1>
                 solutions.classList.add("loading");
                 work = { queue: queue.value, garbage, hold: hold.checked };
                 worker.postMessage(work);
+                showProgress();
             } else {
                 work = { queue: queue.value, garbage, hold: hold.checked };
             }
diff --git a/gomen/worker.js b/gomen/worker.js
index ad5f583..ec5f360 100644
--- a/gomen/worker.js
+++ b/gomen/worker.js
@@ -1,5 +1,12 @@
 importScripts("./pkg/solver.js");
 
+function progress(piece_count, stage, board_idx, board_total) {
+    let stage_progress = board_idx / board_total;
+    let total_progress = (stage + stage_progress) / (2 + 2 * piece_count);
+
+    postMessage({ kind: "progress", amount: total_progress });
+}
+
 async function main() {
     await wasm_bindgen("./pkg/solver_bg.wasm");
     let solver = new wasm_bindgen.Solver();
@@ -34,6 +41,10 @@ async function main() {
 
         let solutions = solver.solve(queue, query.garbage, query.hold).split(",");
 
+        if (solutions[0] == "") {
+            solutions = [];
+        }
+
         postMessage({ kind: "ok", query, solutions });
     }
 }
diff --git a/solver/src/lib.rs b/solver/src/lib.rs
index 6161113..ea8ea9a 100644
--- a/solver/src/lib.rs
+++ b/solver/src/lib.rs
@@ -88,3 +88,8 @@ fn parse_shape(shape: char) -> Option<Shape> {
         _ => None,
     }
 }
+
+#[wasm_bindgen]
+extern "C" {
+    pub fn progress(piece_count: usize, stage: usize, board_idx: usize, board_total: usize);
+}
diff --git a/solver/src/solver.rs b/solver/src/solver.rs
index 3cb0002..4169dfe 100644
--- a/solver/src/solver.rs
+++ b/solver/src/solver.rs
@@ -18,6 +18,7 @@ fn scan(
     legal_boards: &HashSet<Board>,
     start: Board,
     bags: &[Bag],
+    piece_count: usize,
     can_hold: bool,
     place_last: bool,
 ) -> Vec<ScanStage> {
@@ -26,14 +27,19 @@ fn scan(
     let mut prev: ScanStage = HashMap::new();
     prev.insert(start, (bags.first().unwrap().init_hold(), SmallVec::new()));
 
-    for (bag, i) in bags
+    for (stage, (bag, i)) in bags
         .iter()
         .flat_map(|b| (0..b.count).into_iter().map(move |i| (b, i)))
         .skip(1)
+        .enumerate()
     {
         let mut next: ScanStage = HashMap::new();
 
-        for (&old_board, (old_queues, _preds)) in prev.iter() {
+        for (board_idx, (&old_board, (old_queues, _preds))) in prev.iter().enumerate() {
+            if board_idx % 4096 == 0 {
+                crate::progress(piece_count, stage, board_idx, prev.len());
+            }
+
             for shape in Shape::ALL {
                 let is_first = i == 0;
                 let new_queues = bag.take(old_queues, shape, is_first, can_hold);
@@ -67,7 +73,11 @@ fn scan(
     if place_last {
         let mut next: ScanStage = HashMap::new();
 
-        for (&old_board, (old_queues, _preds)) in prev.iter() {
+        for (board_idx, (&old_board, (old_queues, _preds))) in prev.iter().enumerate() {
+            if board_idx % 4096 == 0 {
+                crate::progress(piece_count, piece_count, board_idx, prev.len());
+            }
+
             for shape in Shape::ALL {
                 if old_queues.iter().any(|queue| queue.hold() == Some(shape)) {
                     for (_, new_board) in PiecePlacer::new(old_board, shape) {
@@ -88,6 +98,8 @@ fn scan(
         prev = next;
     }
 
+    crate::progress(piece_count, piece_count, 1, 1);
+
     stages.push(prev);
     stages
 }
@@ -118,20 +130,26 @@ fn place(
     culled: &HashSet<Board>,
     start: BrokenBoard,
     bags: &[Bag],
+    piece_count: usize,
     can_hold: bool,
     place_last: bool,
 ) -> HashMap<BrokenBoard, SmallVec<[QueueState; 7]>> {
     let mut prev = HashMap::new();
     prev.insert(start, bags.first().unwrap().init_hold());
 
-    for (bag, i) in bags
+    for (stage, (bag, i)) in bags
         .iter()
         .flat_map(|b| (0..b.count).into_iter().map(move |i| (b, i)))
         .skip(1)
+        .enumerate()
     {
         let mut next: HashMap<BrokenBoard, SmallVec<[QueueState; 7]>> = HashMap::new();
 
-        for (old_board, old_queues) in prev.iter() {
+        for (board_idx, (old_board, old_queues)) in prev.iter().enumerate() {
+            if board_idx % 4096 == 0 {
+                crate::progress(piece_count, piece_count + 1 + stage, board_idx, prev.len());
+            }
+
             for shape in Shape::ALL {
                 let is_first = i == 0;
                 let new_queues = bag.take(old_queues, shape, is_first, can_hold);
@@ -159,7 +177,11 @@ fn place(
     if place_last {
         let mut next: HashMap<BrokenBoard, SmallVec<[QueueState; 7]>> = HashMap::new();
 
-        for (old_board, old_queues) in prev.iter() {
+        for (board_idx, (old_board, old_queues)) in prev.iter().enumerate() {
+            if board_idx % 4096 == 0 {
+                crate::progress(piece_count, 2 * piece_count + 1, board_idx, prev.len());
+            }
+
             for shape in Shape::ALL {
                 if old_queues.iter().any(|queue| queue.hold() == Some(shape)) {
                     for (piece, new_board) in PiecePlacer::new(old_board.board, shape) {
@@ -174,6 +196,8 @@ fn place(
         prev = next;
     }
 
+    crate::progress(piece_count, 2 * piece_count + 1, 1, 1);
+
     prev
 }
 
@@ -187,12 +211,27 @@ pub fn compute(
         return vec![start.clone()];
     }
 
-    let new_mino_count: u32 = bags.iter().map(|b| b.count as u32 * 4).sum();
+    let piece_count = bags.iter().map(|b| b.count as usize).sum();
+    let new_mino_count = piece_count as u32 * 4;
     let place_last = start.board.0.count_ones() + new_mino_count <= 40;
 
-    let scanned = scan(legal_boards, start.board, bags, can_hold, place_last);
+    let scanned = scan(
+        legal_boards,
+        start.board,
+        bags,
+        piece_count,
+        can_hold,
+        place_last,
+    );
     let culled = cull(&scanned);
-    let mut placed = place(&culled, start.clone(), bags, can_hold, place_last);
+    let mut placed = place(
+        &culled,
+        start.clone(),
+        bags,
+        piece_count,
+        can_hold,
+        place_last,
+    );
 
     let mut solutions: Vec<BrokenBoard> =
         placed.drain().map(|(board, _queue_states)| board).collect();