Improvements KMServer adds on top of Java parity, plus intentional behavior differences. Accepted gaps that are not planned to be closed are listed under Known limitations.
Capabilities the Java version does not have
- Outbound webhooks: library events POSTed as JSON to configured URLs, with HMAC-SHA256 signatures
- History events: scan-triggered trashing and trash emptying recorded in
GET /api/v1/history - Search: simplified ↔ traditional Chinese cross-search, CJK boundary unigrams, a prefix fallback for CJK queries that run past the title, and a fuzzy fallback for typos and near-miss volume numbers
- EPUB 2 series metadata: calibre-style OPF 2
<meta>fallback for series title and number - Natural sort: numbered titles sort by numeric value, with a configurable ICU locale
- komf integration: one-click setup of a komf metadata fetcher
- Reading trackers: per-user OAuth and reading-progress sync to AniList, MyAnimeList, Bangumi, and MangaBaka
- Thumbnail file storage: opt-in file-backed thumbnails instead of DB blobs
- komga-riir databases: adopt databases created by komga-riir
- komga-cn databases: adopt databases created by the komga-cn fork’s Rust port
- Next/previous navigation: a server setting makes the next-book endpoints skip read books
- Recent reading window: a server setting limits Keep Reading and On Deck to series read in the last N days
- Reading statistics: per-user reading totals, streaks, and daily activity
- Smart lists: saved search filters evaluated live per requesting user, with visibility scopes and generated covers
- Library and server statistics: per-library content counts for any user, and an admin server snapshot with task and process state
- Update check: periodic check for new releases, surfaced in the admin UI and
/api/v1/update-check - Session management: users review and revoke their own sessions; administrators see every session’s owner and can kick it
- SSE recovery: after a reconnect, missed events are replayed when the server still holds all of them
Behavior differences
Intentional differences from the Java behavior. Where a setting can restore the Java behavior, the entry names it.
Security and authentication
/actuator/shutdownis not implemented: the Java version lets anyone who can reach the port shut the server down anonymously (management.endpoint.shutdown.access: unrestricted). kmrs has no HTTP shutdown endpoint at all.- EPUB resources on
/api/v1/books/{bookId}/resource/{path}(and the OPDS v2 mirror) require authentication for every file type, and the requester must pass the book’s content-restriction check; anonymous requests get the same bare 401 as the rest of the API. Java permits anonymous access to font files declared in the OPF, bypassing both. /api/v1/fonts/**requires authentication. Java serves/api/v1/fonts/resource/**anonymously: font files of any known family are downloadable without credentials, and family names are enumerable via 200/404 probing.- The KOReader sync endpoints (
/koreader/**) also accept the RESTX-API-Keyheader; Java accepts only the KOReaderX-Auth-User/X-Auth-Keypair there. - Successful API-key authentications record at most one authentication-activity row per key per hour, and successful password authentications at most one per user and device (IP address and user agent) per hour; Java records one per request, which floods the login-activity view for clients that send their key or password on every request. A sign-in from another device is still recorded right away, and failed authentications are still recorded per request.
- The client address in the login activity comes from
X-Forwarded-Foronly when the request arrives from a reverse proxy listed inserver.trusted-proxies(addresses or CIDR ranges); from any other sender the header is ignored and the address of the connection is used. Java takes the first address in the header from anyone, so a client can report any address it likes. By default the list holds loopback and the private ranges (127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7), so a reverse proxy on the same host, in a container network or on the LAN works without configuration, while a sender on a public address is never trusted. A configured list replaces the default: list a proxy on a public address there, or settrusted-proxies = [](KOMGA_TRUSTED_PROXIES=nonein the environment) to ignore the header from everyone, which also stops devices on the private network from reporting an address of their choice. Behind several proxies the address used is the last one in the header that is not a listed proxy. The first time a header is ignored, the server logs one warning that names the sender; it is not repeated until the next start. - Failed password logins are throttled per account and client address; Java has no limit. After 5 wrong passwords in a row from one address, that address is refused password logins for the account for 1 minute, and each further failure after the lock ends doubles it, up to 15 minutes; a correct password clears the count. While locked, a password login gets the same 401 as a wrong password and the attempt is recorded with the error
Too many failed attempts. The account’s other devices, sessions, API keys and remember-me cookies are not affected. The address is the oneserver.trusted-proxiesresolves (previous entry), so clients behind one shared address share a lock. IPv6 clients are counted by their /64 network. If you can’t sign in after several attempts, wait a minute and try again. - At most half the available CPU cores (at least one) check passwords at the same time; Java has no cap. A password login that cannot start its check within 2 seconds gets a 503 with
Retry-After: 1and can simply be retried; it is neither counted as a failure nor recorded. A successful check is remembered for 5 minutes for the same account, client address and password (as a keyed hash in memory, never the password), so clients that send their password on every request are checked once and not on every request. Changing a password immediately stops the old password from authenticating through the cache. The cache remembers password verification, not permissions: every request reads the current account and permissions. Permission changes still revoke sessions, but valid password checks remain until their original five-minute deadline; cache hits do not extend it. A deleted account cannot authenticate through the cache. Known limitation: guesses that come from the same address as such a client are compared with the remembered password until the lock starts, and with the defaultserver.trusted-proxiesa device on the private network can claim that address with a forgedX-Forwarded-For.
API, sessions, and settings
GET /actuator/sessionswithout?username=lists all sessions (Java rejects it).- Stored sessions are bounded. With the default
jdbc-session-enabled: true, each user keeps at most 50 sessions insessions.sqlite, and signing in again beyond that drops the least recently used one; the in-memory store (jdbc-session-enabled: false) holds at most 10,000 sessions in total, least recently used evicted first. Java keeps every session until it expires, so a client that sends credentials on every request without returning its session keeps adding to the store. A dropped session only means that client signs in again. - Request bodies are limited: an uploaded file (a cover, a ComicRack list) may be 2 MiB, and JSON bodies about the same. Java allows 1 MB per uploaded file and 10 MB per upload request (Spring Boot’s defaults) and has no limit on JSON. A body over the limit gets a 413 with a message saying so, and
maxUploadFileSizeByteson/api/v1/settingsreports the limit that is enforced. - Server port, context path, and the kepubify path are configuration-only. The Java version also honors database overrides set from the settings UI (applied on restart); kmrs never reads them —
PATCH /api/v1/settingssilently ignores the three fields, the admin settings UI does not offer them, and stale values left in a Java-era database have no effect. - The
koboPortsetting is removed. Java rewrites the port in Kobo download URLs for direct connections whose Host header lacks one (a Kobo firmware quirk); kmrs builds the URLs from the request’s Host as-is and expects Kobo sync behind a reverse proxy, which supplies the right host and port itself.PATCH /api/v1/settingssilently ignores the field and the admin settings UI no longer offers it. /api/v1/books/{bookId}/nextand/previousbreaknumberSortties by book id; Java’s tie-handling is unstable — see Next/previous navigation.- Log levels are set with one filter in
RUST_LOGsyntax, from[logging] levelinconfig.tomlor theRUST_LOGenvironment variable (which wins when both are set). Java sets levels per logger withlogging.level.*inapplication.yml; kmrs does not read those keys, and the first-start migration does not carry them over.
Scanning and files
- ISBN barcode import is disabled by default for newly created libraries (Java: enabled); barcode scanning requires decoding and inspecting several full-size pages per book. Existing library settings are unchanged, and the option can be enabled per library.
- Book and series file deletion stays asynchronous: requests return 202 after queueing, and completion is reported to admins through
FileDeletionCompletedSSE with the underlying error. The server stats snapshot retains the latest failure per task type for the current process; Java leaves failures in the server log. [libraries] allow-file-deletion = falserefuses deleting book and series files and duplicate pages: the delete endpoints answer 403, queued delete tasks fail with the same message and automatic duplicate page removal is skipped. Java has no such switch; a read-only mount of the library is the usual protection.- File deletion is idempotent: paths that are already missing count as completed, and the remaining cleanup and soft-delete still run, so retries after partial failures can finish. Actual filesystem results replace Java’s
Files.isWritableprecheck, which has different semantics from deletion on Unix. - Hash-based trash restore keeps customized titles: when a moved book or series matches a trashed entry by file hash, a title/titleSort that differs from the old file/directory name (or is locked) is restored with the rest of the metadata; a value that still echoes the old name follows the new one. Java restores locked fields only, losing an unlocked custom title.
- A scan that cannot read a directory (permissions, a network share dropping out) keeps the series and books under it instead of soft deleting them and reads it again on the next scan; a scan that finds no series while some directories failed soft deletes nothing. Java skips the unreadable subtree and soft deletes what was under it.
- A library root without visible (not dot-prefixed) entries, such as a mount point with nothing mounted, is treated like an unreachable one when the library has series: the library is marked unavailable and the scan stops before soft deleting anything. A library without series on an empty folder stays available. Java soft deletes every series.
- An unavailable library is marked available again within a minute of its root becoming readable and not empty, without a scan; changes on disk are picked up by the next scan. Java keeps the library unavailable until the next scan.
- Page order and series book numbering are case-sensitive (Java: case-insensitive).
- Analysis error comments name the cause:
media.commentforERR_1005/ERR_1008appends the underlying error (e.g.ERR_1008: could not open zip archive: ...), so clients show the actual reason instead of a bare “unknown error”; Java stores the bare code. - EPUB files present in the container but undeclared in the OPF
<manifest>(common in repacked light-novel EPUBs) are served on/api/v1/books/{bookId}/resource/{path}: the path is normalized as at analysis time (..escaping the archive root is rejected) and the Content-Type is guessed from the extension. The generated Readium manifest still reflects only the parsed OPF. Java 404s such entries, leaving broken images in the reader.
Thumbnails and covers
- Book/series thumbnails:
If-None-Matchanswered from a deep ETag (blob rows: row id + size; file-backed rows: mtime + size) instead of hashing the body;thumbnails.deep-etag = falserestores Java behavior. - Regenerating a book thumbnail reuses the stored generated thumbnail when its longest edge is at least the configured size, downscaling it instead of reopening the book file (never upscaled; any miss — no stored thumbnail, unknown dimension, stale or unreadable bytes — falls back to source extraction). Only user-initiated regenerations take the shortcut, so a replaced book file still gets its new cover from the file; Java always re-reads the file, and byte output can differ for these thumbnails, same class of deviation as the JPEG encoder.
- Collection/read list covers: adaptive collage of up to 4 member covers (1 full, 2/3 mirrored across the 2×2 diagonal, 4 = 2×2) on a blurred, dimmed blowup of the first cover (neutral gray when no cover is available); Java renders a fixed 2×2 grid on black, cycling members.
Performance and storage
- Memory is bounded regardless of CPU cores: 16 MiB SQLite page-cache budget per database (
cache_sizepragma overrides), andmallocarenas capped at 2 on Linux/glibc (MALLOC_ARENA_MAXoverrides;1saves more on tiny boxes but serializes C-side allocation). synchronousdefaults toNORMALin WAL mode (Java: SQLite’sFULL): commits no longer fsync, so write bursts (komf full-library match, scans) don’t saturate the disk; a power loss can roll back the last committed transactions but cannot corrupt the database.[database.pragmas] synchronous = "FULL"restores Java behavior.- The WAL file is capped at 64 MiB in WAL mode (
journal_size_limitpragma; Java: uncapped): SQLite’s autocheckpoint rewinds the write position but never shrinks the file, so without a cap it sits at the high-water mark of the largest past write burst.[database.pragmas] journal_size_limit = "-1"restores Java behavior. - SQL statements that run 500 ms or longer are logged at WARN with their text (log target
komga_db::slow_query); lock waits count toward the time, so lock-starved writers show up too. The Java version has no slow-query log. task_pool_sizedefaults to 2 (Java: 1): with a single worker, one long task — most notably a search index rebuild — blocks every scan/analyze/hash task behind it; two workers parallelize across series while same-series tasks stay mutually exclusive at the SQL level. Set it back to 1 in the server settings to restore Java behavior.- Page serving caches open container handles behind small LRU bounds keyed by path + size + mtime: PDF documents (4 entries), ZIP/EPUB archives (16), and RAR read positions (2 per thread). Flipping pages no longer re-parses the container per request, and on solid RAR archives each page is decompressed once instead of re-decompressing from the start; a replaced file is detected by its changed key and reopened. The RAR cache is Unix-only because unrar opens archives without delete sharing — on Windows a cached handle would block deleting the book. Java opens and closes the container on every request.