DEV Community

Crane Zhou
Crane Zhou

Posted on

Benchmarking 7 Cloud Storage Providers for Streaming Encrypted Manga on Android (123 ms on Bunny, 30s Stalls on Drive)

When we started building Easemic, an open-source (Apache-2.0) Android comic and manga reader (GitHub repo), we wanted a storage model where users could keep a 200 GB library off their phone's flash storage without running a 24/7 home server.

The idea was simple: when you collect a series or gallery inside the app, the phone encrypts each page on-device and uploads it to a bucket or drive you already own. When you tap Read, the app streams only a sliding window of pages around your current position.

On paper, any cloud storage API that supports HTTP GET and PUT should work. In practice, once we built an automated benchmark harness and ran 10 to 20 back-to-back reading sessions across seven providers from a 300 Mbit/s US fiber connection, the differences in first-page latency, tail stalls, and undocumented rate limits were stark.

Here is what we measured, the three production bugs that broke our early builds, and how S3, WebDAV, and consumer drives actually behave when an Android image loader hits them page by page.


1. First-page latency across 7 providers (measured on device)

We measured the wall-clock time from tapping Read on a cold cloud-only comic to the first decoded bitmap appearing on the Android screen. Each provider was tested in both plaintext (where allowed) and client-side encrypted modes across 10 to 20 runs between September 29 and September 30, 2026:

Provider Protocol Median first page Monthly cost for 200 GB (read 1x/mo) Notes from live testing
Bunny Storage HTTP Storage API 123 ms ~$2.00 Fastest overall; zero egress fees on storage API reads
Cloudflare R2 S3-compatible 199 ms ~$2.85 (10 GB free) Encrypted (199 ms) edged out plaintext (216 ms) within noise
Wasabi S3-compatible 215 ms $7.99 (1 TB minimum) Fast, but bills a 1 TB floor even if you store 50 GB
Backblaze B2 S3-compatible 236 ms ~$1.32 (10 GB free) Cheapest at 200 GB; unverified free accounts hit a 1 GB/day cap
Tigris S3-compatible 347 ms Free up to 5 GB Clean S3 implementation, zero egress fees
Google Drive Drive REST v3 1.1 s Existing quota 22% of sessions hit one ~30 s first-byte stall
OneDrive Microsoft Graph 1.1 s Existing quota Consistent 1.1 s handshake, slower sequential page turns

(Full raw run tables and methodology are published on our cloud reading speed benchmark page and S3 storage pricing breakdown.)

Two numbers surprised us:

  1. Client-side encryption added near-zero overhead. On Android 14 hardware, decrypting a 1.5 MB WebP or JPEG page in memory takes single-digit milliseconds, completely dwarfed by the TLS and HTTP round trip.
  2. Google Drive's 30-second first-byte stalls. While Google Drive's median first page was 1.1 seconds, roughly 22% of our automated reading sessions hit a single media request that hung for ~30.0 seconds before delivering the first byte. Once you flip 40 pages in a row, Drive's per-file download endpoint throttles concurrent bursts in ways object storage never does.

2. Three bugs we hit in production (and how we fixed them)

Dead end #1: Holding decrypted pages in ByteArray windows caused OOM crashes

In our first cloud reader implementation, the prefetch window held the current page plus neighboring pages as decrypted ByteArray buffers in the JVM heap and passed them to Coil 3.

That passed every unit test with synthetic 200 KB test images. Then we opened a 200-page full-color anthology where individual pages and animated GIFs ran 5 to 12 MiB each. Fast-swiping through ten pages pinned multiple multi-megabyte byte arrays across coroutine cancellations, blowing past the Android heap limit.

The fix: We split the cloud reader route at a 4 MiB threshold. Any object >= 4 MiB streams straight through the cipher stream into a bounded on-disk cache file keyed by object digest, and hands Coil a File reference instead of a heap ByteArray. Heap pressure dropped flat even on 15 MB animated pages.

Dead end #2: Komga OPDS cover thumbnails returning 401 Unauthorized

Easemic also acts as a native Android OPDS client for self-hosted Komga and Kavita servers. When we tested against Kavita, every series cover loaded immediately. When we pointed the exact same code at a password-protected Komga 1.28.0 server, the XML catalog parsed, series titles appeared, and every single cover thumbnail was blank.

Why? Kavita embeds the user's API key directly inside the URL path of every <link rel="http://opds-spec.org/image/thumbnail"> element. Komga does not: its thumbnail URLs look like /opds/v1.2/books/{id}/thumbnail and require the Authorization: Basic ... header on every image fetch. Our XML parser sent Basic headers, but handed bare URLs to the UI image loader. Scoping catalog credentials to the image loader fixed it in Easemic 0.2.1.

Dead end #3: WebDAV path quirks and silent password revocations

When testing WebDAV comic streaming on Android against commercial drives (Nutstore, 123pan, Nextcloud), two non-obvious failures cropped up:

  • Bare hostnames like https://webdav.123pan.cn return 404 Not Found on every PROPFIND unless you append /webdav.
  • Backblaze B2 accounts without a payment card on file enforce a hard 1 GB daily download cap; the moment your reader crosses 1 GB in a day, every subsequent page fetch returns 403 Forbidden until midnight GMT.

3. Why we made client-side encryption mandatory on consumer drives

If you store unencrypted comic archives on Google Drive, OneDrive, or consumer WebDAV drives, automated abuse scanners hash and inspect every image. As we documented in our review of Google Drive and Microsoft OneDrive scanning terms, a false positive or policy flag doesn't just delete the file; it can lock your entire Google or Microsoft account.

In Easemic, any library bound to Google Drive, OneDrive, or WebDAV is encrypted on the phone with a password you choose before the first byte leaves the device. On S3-compatible buckets (Cloudflare R2, Bunny, Backblaze B2), you can choose encrypted or plaintext storage, and paste a single API token to auto-provision the bucket.

If you want to inspect the Kotlin implementation, run the benchmark suite yourself, or try the APK:

Top comments (0)