Why is my network scanner so slow?

I scan all my incoming mail into Paperless-ngx. Nothing fancy, just regular letters and correspondence, duplex, 300 dpi, color. The volume isn’t huge, but it annoyed me every single time: a duplex page took 45 seconds to go through the ADF of my Epson WorkForce Pro WF-C5890. That’s a long time to stare at a scanner for a single sheet of paper, and it was bugging me enough to actually chase it down.

The setup

The scanner sits on my WiFi, and the original workflow was scan-to-FTP directly from the printer’s panel into the consume folder of my Paperless-ngx instance, which runs as a rootless Podman/Quadlet stack on an AlmaLinux 10 host (apps1). My first suspect was the obvious one: maybe 300 dpi color is just too much for FTP transfer speed, and I should drop to 200 dpi to save some time.

Turns out that wasn’t it at all.

Ruling out the easy explanations

First I isolated the transfer itself. Sending the finished document via FTP was fast - blink and you’d miss it. So it wasn’t the network transfer to the consume folder. It had to be something happening before that, during the actual scan.

Next I compared color vs. black & white at the same 300 dpi:

  • 300 dpi, color, duplex: ~45 seconds
  • 300 dpi, black & white, duplex: ~35 seconds

So color costs about 10 seconds, but there’s a solid 35 seconds baseline that has nothing to do with the color mode. That ruled out “just switch to grayscale” as a real fix.

Then came the real surprise. I scanned the exact same sheet from my Mac using the Epson Scan Utility - same duplex ADF, same 300 dpi, same color mode - and it was done in 15 seconds. Same scanner, same settings, same physical mechanics. Three times faster.

Chasing ghosts: SANE backends on Linux

Since the panel-to-FTP workflow goes through the printer’s own embedded processor, my next theory was that the panel’s on-device image processing pipeline was the bottleneck, and that a computer-driven scan would bypass it - the same way the Mac apparently did. So I set up scanning directly from apps1 using SANE.

First sane-airscan, talking eSCL to the scanner:

scanimage --device-name="airscan:e0:Epson WF-C5890" \
  --source="ADF Duplex" --mode=Color --resolution=300 \
  --format=tiff --batch=/tmp/page-%04d.tiff --batch-print

45 seconds. No improvement at all.

Then I tried the native ESC/I-2 backend, epsonds, talking directly to port 1865 - the same protocol family the Mac driver presumably uses:

time scanimage --device-name="epsonds:net:192.168.20.210" \
  --source="ADF Duplex" --mode=Color --resolution=300 \
  --format=tiff --batch=/tmp/page-%04d.tiff --batch-print
scanimage ... 0.05s user 0.37s system 0% cpu 44.274 total

Still 44 seconds. At this point I was fairly convinced 45 seconds was just the hard physical limit of this scanner at 300 dpi color duplex, and the Mac’s 15 seconds must have been a fluke, or a different (lower) effective resolution.

I checked that too, of course - never trust a number you haven’t verified. pdfimages -list on the Mac-generated PDF:

page   num  type   width height color comp bpc  enc interp  object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
   1     0 image    2481  3507  rgb     3   8  jpeg   no         9  0   300   300  437K 1.7%

Exactly 300 dpi, no downscaling trick. So the Mac genuinely scanned at full resolution in a third of the time. Something else was going on.

Packet capture time

When you’ve run out of plausible software explanations, it’s time to look at what’s actually going over the wire. I captured traffic on the Mac during a scan (tcpdump -i en7, after figuring out en0 was the wrong interface) and on apps1 during an epsonds scan, both against the scanner’s IP.

The Mac capture told a clean story: a short setup phase, then two big data bursts (front page, back page) separated by a ~3.5 second gap for the ADF to flip the paper, and it was done. Total data transferred for the whole duplex page: 5.87 MB.

The apps1/epsonds capture showed continuous, uninterrupted data flow for 36 straight seconds. Total data transferred for the same duplex page: 67.45 MB. Eleven and a half times more data, for the identical document at the identical settings.

That’s the kind of number that stops being a mystery and starts being a root cause.

FMTJPG vs. FMTRAW

The Epson ESC/I-2 protocol is refreshingly readable on the wire - plain ASCII command frames with a IS header. Both captures contain a PARA command block where the client tells the scanner exactly what it wants. From the Mac:

#ADFDPLX#RSMi0000300#RSSi0000400#COLC024#FMTJPG #JPGd090#GMMUG10...

FMTJPG, quality 90. And from apps1 via epsonds:

#ADFDPLX#COLC024#FMTRAW #GMMUG18#RSMd300#RSSd300...

FMTRAW. Unconpressed. The open-source epsonds SANE backend simply never asks the scanner for JPEG-compressed output - it always requests raw pixel data, and there’s no --format or --compression option exposed to change that (confirmed via scanimage --help -d epsonds:..., which only lists mode, resolution, source and a handful of image enhancement options).

Doing the math: 2481 × 3507 pixels × 3 bytes per pixel (RGB) is about 26 MB per side, uncompressed - which lines up almost exactly with the 67 MB we captured for both sides plus protocol overhead. No mystery left here at all.

What about airscan/eSCL then?

Since epsonds was clearly requesting raw data, I went back and captured airscan too, now that I’d ruled out an unrelated issue where the scanner was still holding a session lock for the Mac’s Epson Scan Utility (yes, that happened too - the scanner embeds the IP of the client currently holding the connection in its rejection response when a second client tries to connect, which is its own small debugging story).

airscan over eSCL turned out to transfer even less data than the Mac - just 1.72 MB total, clearly JPEG-compressed. But it still took about 40 seconds. The capture showed why: thousands of tiny TLS packets, averaging around 425 bytes each, instead of a handful of large TCP segments. The printer’s eSCL/HTTPS scan server appears to stream the JPEG data in small chunks with a lot of round trips, rather than buffering and sending it in bulk the way the native ESC/I-2 socket does. Less data, but death by a thousand small packets.

So: two different open-source SANE backends, two completely different bottlenecks, both landing at roughly the same disappointing result. Neither comes close to what Epson’s own driver does over the exact same network path.

Why is the panel slower than the Mac in the first place?

This whole investigation started with the panel-to-FTP workflow, not with SANE on Linux - so it’s worth closing the loop on that original question too: why would scanning from the printer’s own touchscreen be slower than scanning from a computer at all? It’s the same physical scanner either way.

The most likely answer surfaced almost by accident while chasing something unrelated. Epson discloses in its open source license notices that its printers ship with GPL and LGPL programs, Apple’s Bonjour (mDNS), Net-SNMP, BSD-licensed code and, in some models, linux-ftpd-0.17. That’s a strong indication there’s a full embedded Linux stack inside the device, handling the network side of things - which lines up exactly with what we saw on the wire ourselves: Bonjour/mDNS announcements, an FTP daemon for scan-to-folder, TLS via OpenSSL for eSCL.

Here’s the architectural implication: the CIS sensor, the ADF motor and the duplex mechanism are almost certainly driven by a separate, proprietary firmware component (its own small controller or DSP), not directly by the Linux side. Linux handles the network stack, the web UI, protocol handling for eSCL/ESC-I-2/FTP - and, in all likelihood, the image encoding step, using something like libjpeg. A panel-initiated scan-to-FTP job has to go through this entire pipeline on a comparatively weak embedded CPU: capture the raw sensor data, hand it to the Linux side, encode it, write it as a file, then push it out over FTP. A computer-driven scan can ask the same embedded system for a raw or lightly processed stream and do the heavy lifting (JPEG encoding, PDF assembly) on a much beefier host CPU instead - except, as it turned out with epsonds, the open-source Linux backend didn’t know how to ask for the compressed format, so it ended up on the slow path too, just for a different reason.

In other words: both the panel workflow and my epsonds attempts were probably paying the same underlying tax - image encoding on an underpowered embedded processor - just triggered from different directions. The Mac’s proprietary driver is the only client in this story that reliably avoids that tax by asking for pre-compressed JPEG data and getting it.

The pragmatic fix

At this point I could have gone down the rabbit hole of patching epsonds to request JPEG, or digging into whether Epson’s own Linux driver (epsonscan2) handles network scanning better (spoiler: historically it has a spotty reputation for exactly that). Instead I did the thing that actually gets documents into Paperless faster today: I kept scanning from the Mac, where it’s already fast, and automated the handoff.

A folder action on ~/Documents, triggered by Automator, watches for new scans, waits until the file size has stopped changing (so it doesn’t grab a half-written PDF), and then hands it off:

#!/usr/bin/env bash
# ~/scripts/scan-to-consume.sh

REMOTE_HOST="patrick@apps1"
REMOTE_DIR="/home/patrick/podman/paperless/consume/"
LOG_FILE="${HOME}/Library/Logs/scan-to-consume.log"

for FILE in "$@"; do
  BASENAME="$(basename "${FILE}")"
  [[ "${BASENAME}" != img*.pdf ]] && continue
  [[ ! -f "${FILE}" ]] && continue

  PREV_SIZE=-1
  for i in $(seq 1 15); do
    CUR_SIZE=$(stat -f%z "${FILE}" 2>/dev/null)
    [[ "${CUR_SIZE}" == "${PREV_SIZE}" && -n "${CUR_SIZE}" ]] && break
    PREV_SIZE="${CUR_SIZE}"
    sleep 1
  done

  rsync -az --remove-source-files "${FILE}" "${REMOTE_HOST}:${REMOTE_DIR}" \
    >> "${LOG_FILE}" 2>&1
done

rsync --remove-source-files only deletes the local copy once the transfer has actually succeeded, so a flaky WiFi moment doesn’t cost me a scanned document. The Automator workflow is just a “Folder Action” wrapping this script with “pass input as arguments.”

End result: scan on the Mac at native speed (15 seconds per duplex page), document lands in the Paperless consume folder within a second or two, done. No more staring at the scanner.

Takeaways

  • Never trust “it’s probably the resolution/color mode” until you’ve actually measured it. In my case both were red herrings.
  • When two implementations of the “same” protocol produce wildly different results, packet captures beat speculation every time - and with a text-based protocol like Epson’s ESC/I-2, you don’t even need a protocol dissector, strings and a hex dump will do.
  • Open-source SANE backends are a fantastic and usually good-enough abstraction, but they don’t always implement every optimization a vendor’s proprietary driver does. epsonds requesting raw instead of JPEG, and the scanner’s eSCL implementation streaming in tiny chunks, are two very different root causes that happened to produce the same symptom.
  • Sometimes the right fix isn’t the “purist” one (patch the backend, get everything working natively on Linux) but the pragmatic one that gets you back to scanning documents instead of debugging network protocols.