diff --git a/crates/engine/src/cmd/blocks.rs b/crates/engine/src/cmd/blocks.rs
--- a/crates/engine/src/cmd/blocks.rs
+++ b/crates/engine/src/cmd/blocks.rs
@@ -341,6 +341,8 @@ fn run_wblock(s: &mut Session, p: &Value) -> Result<Value> {
     d.text_styles = src.text_styles.clone();
     d.dim_styles = src.dim_styles.clone();
     d.blocks = src.blocks.clone();
+    // The written drawing keeps the source's units, as AutoCAD's -WBLOCK does.
+    d.header.set_i64("INSUNITS", src.header.i64("INSUNITS", 1));
     // The base point becomes the written drawing's origin, its insertion base: by default 0,0 for
     // objects and the block's base point for a block (`base` is then in block coordinates).
     let base = point_param(p, "base");
@@ -368,6 +370,22 @@ fn run_wblock(s: &mut Session, p: &Value) -> Result<Value> {
             })
             .collect()
     };
+    // Keep only the blocks the written objects use (nested references, dimension geometry and
+    // multileader content, as PURGE follows them) and the dimension styles' arrowhead blocks, and
+    // make new handles start above those of the blocks that stay.
+    let mut frontier: Vec<String> = Vec::new();
+    for e in &ents {
+        purge::block_refs(e, &mut frontier);
+    }
+    for st in &d.dim_styles {
+        frontier.extend([&st.arrow_block, &st.arrow_block1, &st.arrow_block2, &st.leader_arrow_block].into_iter().cloned());
+    }
+    let used = purge::reachable_blocks(&d, frontier);
+    d.blocks.retain(|_, b| used.contains(&b.name.to_ascii_uppercase()));
+    let kept: Vec<Handle> = d.blocks.values().flat_map(|b| b.entities.iter().map(|e| e.handle)).collect();
+    for h in kept {
+        d.bump_handseed(h);
+    }
     for mut e in ents {
         e.handle = d.new_handle();
         d.model.push(e);
diff --git a/crates/engine/src/cmd/blocks/purge.rs b/crates/engine/src/cmd/blocks/purge.rs
--- a/crates/engine/src/cmd/blocks/purge.rs
+++ b/crates/engine/src/cmd/blocks/purge.rs
@@ -6,7 +6,7 @@
 
 use std::collections::{HashSet, VecDeque};
 
-use cadcraft_doc::{Drawing, EntityKind, Handle, STANDARD_MLINE_STYLE};
+use cadcraft_doc::{Drawing, Entity, EntityKind, Handle, STANDARD_MLINE_STYLE};
 use serde_json::{Map, Value, json};
 
 use super::super::qselect::wildcard;
@@ -117,18 +117,20 @@ fn entities(d: &Drawing) -> impl Iterator<Item = &std::sync::Arc<cadcraft_doc::E
     d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())).chain(d.blocks.values().flat_map(|b| b.entities.iter())).take(5_000_000)
 }
 
-/// Upper-case names of blocks reachable from model space, the layouts and dimensions.
-fn used_blocks(d: &Drawing) -> HashSet<String> {
-    let mut used = HashSet::new();
-    let mut frontier: Vec<String> = Vec::new();
-    for e in d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())) {
-        match &e.kind {
-            EntityKind::Insert(i) => frontier.push(i.block.clone()),
-            EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),
-            EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),
-            _ => {}
-        }
+/// Adds the names of the blocks `e` refers to directly (an insert's block, a dimension's geometry
+/// block, a multileader's block content) to `out`.
+pub(super) fn block_refs(e: &Entity, out: &mut Vec<String>) {
+    match &e.kind {
+        EntityKind::Insert(i) => out.push(i.block.clone()),
+        EntityKind::Dimension(dm) => out.extend(dm.block.clone()),
+        EntityKind::MLeader(m) => out.extend(m.block.as_ref().map(|b| b.block.clone())),
+        _ => {}
     }
+}
+
+/// Upper-case names of the blocks in `frontier` and of those they refer to, nested.
+pub(super) fn reachable_blocks(d: &Drawing, mut frontier: Vec<String>) -> HashSet<String> {
+    let mut used = HashSet::new();
     let mut guard = 0;
     while let Some(n) = frontier.pop() {
         guard += 1;
@@ -137,18 +139,22 @@ fn used_blocks(d: &Drawing) -> HashSet<String> {
         }
         if let Some(b) = d.block(&n) {
             for e in b.entities.iter() {
-                match &e.kind {
-                    EntityKind::Insert(i) => frontier.push(i.block.clone()),
-                    EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),
-                    EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),
-                    _ => {}
-                }
+                block_refs(e, &mut frontier);
             }
         }
     }
     used
 }
 
+/// Upper-case names of blocks reachable from model space, the layouts and dimensions.
+fn used_blocks(d: &Drawing) -> HashSet<String> {
+    let mut frontier: Vec<String> = Vec::new();
+    for e in d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())) {
+        block_refs(e, &mut frontier);
+    }
+    reachable_blocks(d, frontier)
+}
+
 fn is(name: &str, list: &[&str]) -> bool {
     list.iter().any(|n| n.eq_ignore_ascii_case(name))
 }
diff --git a/crates/engine/tests/wblock_units_handles.rs b/crates/engine/tests/wblock_units_handles.rs
new file mode 100644
--- /dev/null
+++ b/crates/engine/tests/wblock_units_handles.rs
@@ -0,0 +1,57 @@
+//! WBLOCK writes the source's units, only the blocks the written objects use, and handles that
+//! never repeat.
+
+use cadcraft_engine::Session;
+use cadcraft_engine::cmd::file::{IoHooks, set_io};
+use cadcraft_engine::doc::Drawing;
+use serde_json::json;
+
+/// Writes "insunits;block names;handles;distinct handles" so the test can read what would be saved.
+fn write(d: &Drawing, _path: &str) -> Result<Vec<u8>, String> {
+    let mut handles: Vec<u64> = d.model.iter().map(|e| e.handle.0).collect();
+    let mut names = Vec::new();
+    for b in d.blocks.values() {
+        names.push(b.name.clone());
+        handles.extend(b.entities.iter().map(|e| e.handle.0));
+    }
+    let total = handles.len();
+    handles.sort_unstable();
+    handles.dedup();
+    Ok(format!("{};{};{};{}", d.header.i64("INSUNITS", -1), names.join(","), total, handles.len()).into_bytes())
+}
+
+fn written(s: &mut Session, p: serde_json::Value) -> String {
+    let path = std::env::temp_dir().join(format!("cadcraft-wblock-units-{}.dxf", std::process::id()));
+    let mut p = p;
+    p["path"] = json!(path.to_string_lossy());
+    s.execute("wblock", &p).unwrap();
+    let text = std::fs::read_to_string(&path).unwrap();
+    let _ = std::fs::remove_file(&path);
+    text
+}
+
+fn line(s: &mut Session, y: f64) -> String {
+    s.execute("line", &json!({ "points": [[0, y], [1, y]] })).unwrap()["handles"][0].as_str().unwrap().to_string()
+}
+
+#[test]
+fn wblock_keeps_units_drops_unused_blocks_and_repeats_no_handle() {
+    set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write, plot: None });
+    let mut s = Session::new();
+    s.execute("units", &json!({ "insunits": 6 })).unwrap();
+    // Block "Part" is made first, so its line has one of the drawing's lowest handles.
+    let p = line(&mut s, 0.0);
+    let ins = s.execute("block", &json!({ "name": "Part", "handles": [p], "keep": "convert" })).unwrap()["insert"].as_str().unwrap().to_string();
+    let mut picked = vec![ins];
+    for i in 1..300 {
+        picked.push(line(&mut s, f64::from(i) * 0.01));
+    }
+    let u = line(&mut s, 5.0);
+    s.execute("block", &json!({ "name": "Unused", "handles": [u], "keep": "delete" })).unwrap();
+
+    // 300 objects (the insert and 299 lines) plus Part's line: the source's units, no Unused,
+    // and every handle distinct although the new objects are numbered from the start again.
+    assert_eq!(written(&mut s, json!({ "handles": picked })), "6;Part;301;301");
+    // A block written by name: its objects go to model space, its definition stays behind.
+    assert_eq!(written(&mut s, json!({ "name": "Unused" })), "6;;1;1");
+}
