diff --git a/crates/print/README.md b/crates/print/README.md
--- a/crates/print/README.md
+++ b/crates/print/README.md
@@ -117,8 +117,9 @@ It runs formatting, Clippy and this crate's own tests on Windows, Linux and macO
 code changes, and supports manual runs. The Windows job executes the spooler regression above;
 physical printer fidelity still requires hardware verification.
 
-Windows discovery reports driver duplex support in `Printer::two_sided` (a two-sided option in
-the driver's PrintCapabilities); CUPS leaves it unknown. Explicit simplex/duplex choices that the driver
+Discovery leaves `Printer::two_sided` unknown, so listing printers never waits on a driver.
+On Windows, `two_sided(printer)` reads one printer's driver duplex support (a two-sided option in
+its PrintCapabilities); elsewhere it returns `None`. Explicit simplex/duplex choices that the driver
 drops are refused before submission.
 
 Windows dependencies: the maintained [`winprint` fork](https://github.com/storytold/winprint.rs)
diff --git a/crates/print/src/lib.rs b/crates/print/src/lib.rs
--- a/crates/print/src/lib.rs
+++ b/crates/print/src/lib.rs
@@ -49,7 +49,8 @@ pub struct Printer {
     /// `_192_0_2_15` described as `192.0.2.15`. Windows names are already human: empty there.
     pub description: String,
     pub default: bool,
-    /// Whether the Windows driver reports duplex support; None when unknown (CUPS).
+    /// Whether the Windows driver reports duplex support; None when unknown (CUPS, and
+    /// [`printers`] on Windows, which doesn't read it: see [`two_sided`]).
     pub two_sided: Option<bool>,
 }
 
@@ -411,6 +412,23 @@ pub fn printers() -> Vec<Printer> {
     }
 }
 
+/// Whether `printer`'s driver takes two-sided settings from other programs (Windows: its
+/// PrintCapabilities offer a two-sided option); `None` when that can't be read, or off Windows.
+/// [`printers`] leaves [`Printer::two_sided`] unknown, because reading the capabilities of a
+/// printer that can't be reached, such as a WSD printer on another network, takes Windows about
+/// 48 s. Ask for the printer the user picks, off the UI thread.
+pub fn two_sided(printer: &str) -> Option<bool> {
+    #[cfg(windows)]
+    {
+        windows::two_sided_named(printer)
+    }
+    #[cfg(not(windows))]
+    {
+        let _ = printer;
+        None
+    }
+}
+
 /// The Print dialog's printers from lpstat's two answers: the spooler's queues (`-p -d`), then
 /// the driverless destinations of `available` (`-e`) that have no queue — CUPS builds a temporary
 /// queue when `lp` sends them a job. Each destination once, queues first; the default still comes
diff --git a/crates/print/src/tests/windows_native.rs b/crates/print/src/tests/windows_native.rs
--- a/crates/print/src/tests/windows_native.rs
+++ b/crates/print/src/tests/windows_native.rs
@@ -410,6 +410,7 @@ fn windows_jobs_reach_the_spooler() {
         return;
     }
     let listed = spool::printers().into_iter().find(|p| p.name == name);
+    let duplex = spool::two_sided(name);
     let mixed = test_pdf(&[LETTER, LETTER_LANDSCAPE, LETTER], true);
     let job = Job { printer: Some(name.into()), title: "mixed.pdf".into(), ..Job::default() };
     let caller = std::thread::current().id();
@@ -422,7 +423,7 @@ fn windows_jobs_reach_the_spooler() {
         observed.borrow_mut().push((done, total));
     });
     let mut duplex_progress = Vec::new();
-    let duplex_refused = if listed.as_ref().is_some_and(|p| p.two_sided == Some(false)) {
+    let duplex_refused = if duplex == Some(false) {
         Some(spool::submit_with_progress(&mixed, &Job { duplex: Some(Duplex::LongEdge), ..job.clone() }, &mut |done, total| {
             duplex_progress.push((done, total));
         }))
@@ -430,7 +431,7 @@ fn windows_jobs_reach_the_spooler() {
         None
     };
     // The Print dialog's default, an explicit one-sided request, on a queue with no duplex unit.
-    let simplex_only = listed.as_ref().is_some_and(|p| p.two_sided == Some(false));
+    let simplex_only = duplex == Some(false);
     let one_sided = simplex_only.then(|| spool::submit(&mixed, &Job { duplex: Some(Duplex::Off), title: "simplex".into(), ..job.clone() }));
     let tabloid = test_pdf(&[TABLOID, LETTER], false);
     let shrink = Job { printer: Some(name.into()), scaling: Scaling::ShrinkToFit, title: "shrink".into(), ..Job::default() };
@@ -449,7 +450,8 @@ fn windows_jobs_reach_the_spooler() {
         "Get-PrintJob -PrinterName $name | Remove-PrintJob; foreach ($i in 1..20) { if (-not (Get-PrintJob -PrinterName $name)) { break }; Start-Sleep -Milliseconds 250 }; Remove-Printer -Name $name; if (Get-Printer -Name $name -ErrorAction SilentlyContinue) { throw 'the printer is still there' }; exit 0",
         name,
     );
-    assert!(listed.as_ref().is_some_and(|p| p.two_sided.is_some()), "the new printer is listed, its duplex support known: {listed:?}");
+    assert!(listed.as_ref().is_some_and(|p| p.two_sided.is_none()), "the new printer is listed without reading its driver: {listed:?}");
+    assert!(duplex.is_some(), "its duplex support is known when asked for: {duplex:?}");
     assert_eq!(sent.as_deref(), Ok("spooled 3 sheet(s)"), "{sent:?}");
     if let Some(refused) = duplex_refused {
         assert!(matches!(refused, Err(Error::Spool(ref m)) if m.contains("simplex/duplex") && m.contains("nothing was sent")), "{refused:?}");
diff --git a/crates/print/src/windows.rs b/crates/print/src/windows.rs
--- a/crates/print/src/windows.rs
+++ b/crates/print/src/windows.rs
@@ -3,12 +3,12 @@
 //! PowerShell in Constrained Language Mode; storytold/pdfcraft#1107).
 //!
 //! The printers come from `EnumPrinters` (local queues and the user's connections, such as
-//! `\\server\queue`), the default from `GetDefaultPrinter`, and whether each driver takes
-//! two-sided settings from its PrintCapabilities. A job is drawn by the in-box
-//! `Windows.Data.Pdf` with Direct2D into the XPS print API, as vectors, and its options (copies,
-//! collation, two-sided, colour, paper, orientation, no driver scaling) travel as a PrintTicket
-//! that the driver validates before anything is sent. `winprint` and `winsafe` wrap the Win32
-//! and WinRT calls, so there is no `unsafe` here.
+//! `\\server\queue`) and the default from `GetDefaultPrinter`; whether a driver takes two-sided
+//! settings comes from its PrintCapabilities, read only for the printer asked about. A job is
+//! drawn by the in-box `Windows.Data.Pdf` with Direct2D into the XPS print API, as vectors, and
+//! its options (copies, collation, two-sided, colour, paper, orientation, no driver scaling)
+//! travel as a PrintTicket that the driver validates before anything is sent. `winprint` and
+//! `winsafe` wrap the Win32 and WinRT calls, so there is no `unsafe` here.
 
 use std::io::Write as _;
 
@@ -29,13 +29,22 @@ fn default_printer() -> Option<String> {
     winsafe::GetDefaultPrinter().ok().filter(|n| !n.is_empty())
 }
 
+/// The queues and the default only. Each driver's capabilities are left unread: Windows takes
+/// about 48 s to give up on a printer it can't reach (a WSD printer on another network), and
+/// reading every printer's made the list wait for each one in turn (storytold/pdfcraft#2192).
+/// [`two_sided_named`] reads them for the printer that is picked.
 pub(crate) fn printers() -> Vec<Printer> {
     match PrinterDevice::all() {
-        Ok(devices) => crate::printer_list(devices.iter().map(|d| (d.name().to_string(), two_sided(d))), default_printer().as_deref()),
+        Ok(devices) => crate::printer_list(devices.iter().map(|d| (d.name().to_string(), None)), default_printer().as_deref()),
         Err(_) => Vec::new(),
     }
 }
 
+/// [`two_sided`] for the queue named `name`; `None` when there is no such queue.
+pub(crate) fn two_sided_named(name: &str) -> Option<bool> {
+    PrinterDevice::all().ok()?.iter().find(|d| d.name() == name).and_then(two_sided)
+}
+
 /// Whether the printer's driver takes two-sided settings: its PrintCapabilities offer a two-sided
 /// option for the feature the job's ticket sets. `None` when the capabilities can't be read. A
 /// driver without one cannot confirm an explicit two-sided request, which is refused before submission.
