ktor-persistent-cache
1.1.0indexedPersistent HTTP caching for Ktor HttpClient with disk-backed storage, configurable TTL and max size, LRU eviction, Vary-header aware variants, and optional custom cache-directory provider.
Persistent HTTP caching for Ktor HttpClient with disk-backed storage, configurable TTL and max size, LRU eviction, Vary-header aware variants, and optional custom cache-directory provider.
A Kotlin Multiplatform library that adds persistent HTTP caching to Ktor HttpClient via an idiomatic client plugin DSL, with a storage backend you choose explicitly — Okio (default, stable) or kotlinx-io (experimental) — configurable size limits, TTL, and platform-appropriate cache directories.
Upgrading from a pre-1.2 version? The public API moved to a multi-module layout and a new
install(PersistentCache) { ... }DSL. See docs/MIGRATION.md — it leads with the one unavoidable breaking change (a customCacheDirectoryProviderneeds a manual rewrite) before covering everything else, which is a source- and binary-compatible deprecation.
| Platform | Cache directory |
|---|---|
| Android | Application cache dir () |
ktor-client-core 3.4.0+) and an engine (CIO, OkHttp, etc.) for your
targetsThis library ships as four Maven artifacts. Pick one of the two paths below.
Path A — just depend on ktor-persistent-cache (recommended for most users): it transitively
pulls in cache-core and the default cache-okio backend, so install(PersistentCache) { ... }
works with no extra setup.
dependencies {
commonMain.dependencies {
implementation("io.github.santimattius:ktor-persistent-cache:1.2.0")
}
// Also add a Ktor engine for each target, e.g.:
// implementation("io.ktor:ktor-client-okhttp") // Android
// implementation("io.ktor:ktor-client-cio") // iOS / JVM
}
Path B — depend on cache-core plus a backend directly, without the :shared facade — use
this if you want the experimental cache-kotlinx-io backend instead, or want the smallest
possible dependency surface:
dependencies {
commonMain.dependencies {
implementation("io.github.santimattius:ktor-persistent-cache-core:1.2.0")
implementation("io.github.santimattius:ktor-persistent-cache-okio:1.2.0")
// or, instead of the line above:
// implementation("io.github.santimattius:ktor-persistent-cache-kotlinx-io:1.2.0")
}
}
# gradle/libs.versions.toml
[versions]
ktorPersistentCache = "1.2.0"
= { group = , name = , version.ref = }
= { group = , name = , version.ref = }
= { group = , name = , version.ref = }
= { group = , name = , version.ref = }
repositories {
mavenCentral()
// For snapshots:
// maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
The library needs the application context to resolve the cache directory. The recommended way is App Startup:
The (or Android) module that depends on should merge the library's AndroidManifest so that the App Startup and (now in ) are registered.
If you don't use the library's manifest (e.g. you use a different DI or startup path), you must call once at app startup with the application context (not an Activity context):
import io.github.santimattius.persistent.cache.startup.injectContext
// e.g. in Application.onCreate()
injectContext(applicationContext)
injectContext is a public API in io.github.santimattius.persistent.cache.startup. It throws
IllegalArgumentException if you pass a context that can leak memory (for example an Activity).
No setup. The library uses the default app caches directory.
No setup. The library uses a subdirectory of the JVM temp directory.
install(PersistentCache):import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.github.santimattius.persistent.cache.*
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
directory = "http_cache"
maxSize = 10L * 1024 *
ttl = * *
shared =
=
fileSystem = OkioCacheFileSystem()
}
}
fileSystem is required: cache-core ships zero I/O dependencies by design, so you choose a
backend explicitly (see Choosing a backend below). fileSystem and
are typed against a backend SPI annotated — not a
bug, an intentional signal that the SPI itself isn't a stability-guaranteed surface for application
code the way the rest of is. Add
where you call .
Use the client as usual. The cache stores responses for requests that support caching and serves them when valid.
Cross-restart persistence depends on the origin server's cacheability headers per RFC 7234 (for example or ). This library persists whatever Ktor's plugin stores; it does not override freshness rules. Responses marked are not written to disk.
val response: String = client.get("https://example.com/api/data").body()
CacheDirectoryProvider via directoryProvider
(see Custom cache directory).PersistentCacheConfig (configured inside install(PersistentCache) { ... }) supports:
There is no direct enabled toggle: to disable caching, omit install(PersistentCache) entirely.
This library installs Ktor's HttpCache and does not intercept the Auth pipeline. When you also install Auth, behavior follows Ktor's cache routing:
These rules are verified by AuthPluginInteropTest (in cache-okio's test suite, exercised against
the real install(PersistentCache) DSL). No extra configuration is required beyond matching server
cache headers and the PersistentCacheConfig flags above.
cache-core and storage backendsThe library is split across four published modules:
Both backends implement the same CacheFileSystem<P> contract, pass the same shared conformance
test suite, and produce byte-identical on-disk cache filenames for the same input (SHA-256-based
keys) — switching backends does not invalidate an existing on-disk cache.
cache-okio — default, stable, ships transitively via ktor-persistent-cache:
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
directory = "http_cache"
fileSystem = OkioCacheFileSystem() // defaults to okio.FileSystem.SYSTEM
}
}
cache-kotlinx-io — experimental, opt-in, not pulled in by ktor-persistent-cache; add the
ktor-persistent-cache-kotlinx-io artifact directly (see Installation, Path B):
@OptIn(InternalPersistentCacheApi::class, ExperimentalKotlinxIoCache::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
directory = "http_cache"
fileSystem = KotlinxIoCacheFileSystem() // defaults to kotlinx.io.files.SystemFileSystem
}
}
KotlinxIoCacheFileSystem requires the extra ExperimentalKotlinxIoCache opt-in (WARNING-level)
because it tracks kotlinx-io's own Alpha-stability kotlinx.io.files package and may change shape
between minor versions of this library. Okio remains the recommended default backend for
production use; choose cache-kotlinx-io only if you already depend on kotlinx-io and want to avoid
pulling in Okio.
See docs/MIGRATION.md if you're upgrading from a version that used
CacheStorageFactory/CacheConfig directly.
To control where the cache is stored (e.g. a custom folder or test directory), implement
CacheDirectoryProvider and assign it to directoryProvider:
val customProvider = object : CacheDirectoryProvider {
override val cacheDirectory: String get() = "/custom/cache/dir"
}
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
directory = "http_cache"
fileSystem = OkioCacheFileSystem()
directoryProvider = customProvider
}
}
Default behavior (no custom provider): getCacheDirectoryProvider()
returns the platform implementation (Android app cache dir, iOS caches dir, or JVM temp dir).
Note cacheDirectory here is a plain String, not an okio.Path — see
docs/MIGRATION.md if you're upgrading a pre-1.2 custom provider.
Build:
./gradlew build
Run tests and API checks (all modules):
./gradlew check apiCheck
Publish to local Maven:
./gradlew publishToMavenLocal
Then depend on io.github.santimattius:ktor-persistent-cache:1.2.0 (or one of the other three
artifacts) with mavenLocal() in your project.
Each publishable module (shared, cache-core, cache-okio, cache-kotlinx-io) is published with
the gradle-maven-publish-plugin, and
gated by a committed binary-compatibility-validator
baseline (apiCheck) — unreviewed public API changes fail CI.
| Action | Command / Doc |
|---|---|
| Publish to local Maven | ./gradlew publishToMavenLocal |
| Publish to Maven Central | See docs/PUBLISHING.md for credentials and steps. |
Coordinates and POM are configured in each module's build.gradle.kts.
This project is licensed under the Apache License, Version 2.0.
install(PersistentCache) { ... } client plugin built on
Ktor's own HttpCache; you configure storage and
options in one place.cache-okio (default, stable) or
cache-kotlinx-io (experimental, opt-in), both implementing the same
CacheFileSystem SPI on top of one shared caching algorithm in
cache-core, so switching backends does not invalidate an existing on-disk cache.Vary headers so different variants (e.g. by
Accept-Language) are cached separately.CacheDirectoryProvider for
custom cache root paths (e.g. for tests or special directories).context.cacheDir| iOS | App caches directory (NSCachesDirectory in the sandbox) |
| JVM | java.io.tmpdir/ktor-cache |
sharedktor-persistent-cacheInitializationProviderContextInitializercache-coreNo extra code
If the manifest is merged, ContextInitializer runs at app startup and injects the application
context. getCacheDirectoryProvider()
will then use it automatically.
directoryProvider@InternalPersistentCacheApiPersistentCacheConfig@OptIn(InternalPersistentCacheApi::class)install(PersistentCache)Cache-Control: max-age=…Expiresno-store| Property | Type | Default | Description |
|---|
directory | String | "http_cache" | Name of the cache directory under the platform cache root. |
maxSize | Long | 10 MB | Maximum cache size in bytes. LRU eviction when exceeded. Use 0 for no limit. |
ttl | Long | 1 hour | Time-to-live for entries in milliseconds. Values <= 0 mean entries never expire (same convention as maxSize <= 0 = unlimited). |
shared | Boolean | true | Whether the cache is shared across requests (Ktor HttpCache behavior). |
public | Boolean | false | When true, cached responses are treated as public (shareable across users); when false, they are private to the client. |
fileSystem | CacheFileSystem<*>? | null | Required. The storage backend — e.g. OkioCacheFileSystem() or KotlinxIoCacheFileSystem(). @InternalPersistentCacheApi. |
directoryProvider | CacheDirectoryProvider? | null | Optional custom cache root; defaults to the platform-specific getCacheDirectoryProvider(). @InternalPersistentCacheApi. |
clock | () -> Long | getTimeMillis() | Supplies the current time; override for deterministic tests. |
Cache-Control: private (for example private, max-age=3600) to
be stored in private storage — the backend-persisted store configured by
install(PersistentCache) when public = false. Responses with only max-age (no private)
route to Ktor's default public storage, which this plugin does not configure.shared = false on PersistentCacheConfig when using Bearer (or other) auth on the same
client. Ktor skips cache lookup for authorized requests on a shared client and refuses to store
private entries when shared = true.| Module | Artifact | Contains |
|---|
cache-core | ktor-persistent-cache-core | The install(PersistentCache) { ... } DSL, the shared caching algorithm (FileCacheStorage), the CacheFileSystem backend SPI, and CacheDirectoryProvider. Zero I/O dependencies — no Okio, no kotlinx-io. |
cache-okio | ktor-persistent-cache-okio | OkioCacheFileSystem — the default, stable backend, implementing CacheFileSystem<okio.Path>. |
cache-kotlinx-io | ktor-persistent-cache-kotlinx-io | KotlinxIoCacheFileSystem — an experimental, opt-in backend, implementing CacheFileSystem<kotlinx.io.files.Path>. |
ktor-persistent-cache (the :shared module) | ktor-persistent-cache | A compatibility facade depending on cache-core + cache-okio, keeping the pre-1.2 Maven coordinate working. Also where ContextInitializer's Android manifest merge lives (via cache-core). |
| Resource | URL |
|---|
| Migration guide (1.2.0) | docs/MIGRATION.md |
| Ktor — HTTP client | ktor.io/docs/client |
| Ktor — Caching | ktor.io/docs/client-caching |
| Okio | github.com/square/okio |
| kotlinx-io | github.com/Kotlin/kotlinx-io |
| Kotlin Multiplatform | kotlinlang.org/docs/multiplatform |
| Publishing (this repo) | docs/PUBLISHING.md |
Surfaced from shared tags and platforms — no rankings paid for.