
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.
Persistent HTTP caching for Ktor HttpClient on Kotlin Multiplatform.
Responses cached by Ktor's HttpCache are stored on
disk, so they survive app restarts. You choose the storage backend —
Okio (stable, default) or
kotlinx-io (experimental).
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
fileSystem = OkioCacheFileSystem()
}
}Status:
2.0.0-alpha02is a pre-release. The API may still change before2.0.0; see the CHANGELOG. Upgrading from a pre-1.2 version? Read the migration guide first — one change (a customCacheDirectoryProvider) needs a manual rewrite.
HttpCache factories,
custom directory, Auth
1. Add the dependency (plus a Ktor engine per target):
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.santimattius:ktor-persistent-cache:2.0.0-alpha02")
}
}
}2. Install the plugin:
import io.github.santimattius.persistent.cache.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
directory = "http_cache"
maxSize = 10L * 1024 * 1024 // 10 MB
ttl = 60L * 60 * 1000 // 1 hour
fileSystem = OkioCacheFileSystem()
}
}3. Use the client as usual:
val body: String = client.get("https://example.com/api/data").body()4. On Android only, make sure the library can resolve the application context — see Platform setup.
Notes:
fileSystem is required. cache-core ships no I/O dependencies by design, so the backend is
explicit.fileSystem and directoryProvider are typed against a backend SPI annotated
@InternalPersistentCacheApi. That is an intentional signal that the SPI is not a
stability-guaranteed surface, hence the @OptIn where you call install(PersistentCache).
If you would rather avoid the opt-in, use the
HttpCache factories.| Feature | What you get |
|---|---|
| Persistent storage | Cached responses live on the filesystem and survive app restarts. |
| Kotlin Multiplatform | One API for Android, iOS and JVM. |
| Idiomatic Ktor plugin |
install(PersistentCache) { ... }, built on Ktor's own HttpCache. |
| Choice of backend |
cache-okio (stable) or cache-kotlinx-io (experimental). Both share one caching engine and produce identical on-disk files, so switching backends keeps the cache valid. |
| Configurable | TTL, max size, directory name, shared vs. unshared, public vs. private storage. |
Vary-aware |
Variants (for example by Accept-Language) are cached separately. |
| LRU eviction | Least-recently-used entries are removed once the size limit is exceeded. |
| Crash-safe writes | Entries are written atomically and carry a format version. See Storage behavior. |
| Custom location | Optional CacheDirectoryProvider for tests or special directories. |
| API stability gate | Every published module is checked by binary-compatibility-validator; unreviewed public API changes fail CI. |
| Platform | Cache directory | Requirement |
|---|---|---|
| Android | Application cache dir (context.cacheDir) |
minSdk 24+, JVM target 17 |
| iOS | App caches directory (NSCachesDirectory, inside the sandbox) |
iosArm64, iosSimulatorArm64, iosX64
|
| JVM | java.io.tmpdir/ktor-cache |
JDK 17+ |
Built and tested with Kotlin 2.4.20 and Ktor 3.6.0 (ktor-client-core). You also need a Ktor
engine for each target (CIO, OkHttp, Darwin, ...).
The library ships as four Maven artifacts. Pick one path.
ktor-persistent-cache 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:2.0.0-alpha02")
}
// Also add a Ktor engine for each target, e.g.:
// implementation("io.ktor:ktor-client-okhttp") // Android
// implementation("io.ktor:ktor-client-cio") // JVM
// implementation("io.ktor:ktor-client-darwin") // iOS
}Use this for the experimental cache-kotlinx-io backend, or for the smallest dependency surface.
dependencies {
commonMain.dependencies {
implementation("io.github.santimattius:ktor-persistent-cache-core:2.0.0-alpha02")
implementation("io.github.santimattius:ktor-persistent-cache-okio:2.0.0-alpha02")
// or, instead of the line above:
// implementation("io.github.santimattius:ktor-persistent-cache-kotlinx-io:2.0.0-alpha02")
}
}gradle/libs.versions.toml)[versions]
ktorPersistentCache = "2.0.0-alpha02"
[libraries]
ktor-persistent-cache = { group = "io.github.santimattius", name = "ktor-persistent-cache", version.ref = "ktorPersistentCache" }
ktor-persistent-cache-core = { group = "io.github.santimattius", name = "ktor-persistent-cache-core", version.ref = "ktorPersistentCache" }
ktor-persistent-cache-okio = { group = "io.github.santimattius", name = "ktor-persistent-cache-okio", version.ref = "ktorPersistentCache" }
ktor-persistent-cache-kotlinx-io = { group = "io.github.santimattius", name = "ktor-persistent-cache-kotlinx-io", version.ref = "ktorPersistentCache" }Artifacts are published to Maven Central:
repositories {
mavenCentral()
}| Platform | Setup |
|---|---|
| Android | Needs the application context — see below. |
| iOS | None. Uses the default app caches directory. |
| JVM | None. Uses a subdirectory of the JVM temp directory. |
The library needs the application context to resolve the cache directory. The recommended way is App Startup:
ktor-persistent-cache must merge
the library's AndroidManifest, so the App Startup InitializationProvider and
ContextInitializer (in cache-core) are registered.ContextInitializer runs at app startup and injects the application context.
getCacheDirectoryProvider()
then uses it automatically.If you do not use the library's manifest (for example you use a different DI or startup path), call this 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 public API in io.github.santimattius.persistent.cache.startup. It throws
IllegalArgumentException if given a context that can leak memory, such as an Activity.
Options of PersistentCacheConfig, set inside install(PersistentCache) { ... }:
| Property | Type | Default | Description |
|---|---|---|---|
fileSystem |
CacheFileSystem<*>? |
null |
Required. The storage backend, e.g. OkioCacheFileSystem() or KotlinxIoCacheFileSystem(). @InternalPersistentCacheApi. |
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. <= 0 means no limit. |
ttl |
Long |
1 hour | Time-to-live for entries, in milliseconds. <= 0 means entries never expire. |
shared |
Boolean |
true |
Whether the cache is shared across requests (Ktor HttpCache behavior). |
public |
Boolean |
false |
When true, responses are treated as public (shareable across users); when false, private to the client. |
directoryProvider |
CacheDirectoryProvider? |
null |
Optional custom cache root; defaults to the platform getCacheDirectoryProvider(). @InternalPersistentCacheApi. |
clock |
() -> Long |
getTimeMillis() |
Supplies the current time; override for deterministic tests. |
There is no enabled toggle: to disable caching, omit install(PersistentCache).
Both backends implement the same CacheFileSystem<P> contract, pass the same conformance test
suite, and write byte-identical file names (SHA-256-based keys).
cache-okio |
cache-kotlinx-io |
|
|---|---|---|
| Stability | Stable (default) | Experimental |
Pulled in by ktor-persistent-cache
|
Yes | No — add the artifact (Path B) |
| Extra opt-in | None |
ExperimentalKotlinxIoCache (WARNING level) |
| Choose it when | Almost always | You already depend on kotlinx-io and want to avoid Okio |
// Okio (default)
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
fileSystem = OkioCacheFileSystem() // defaults to okio.FileSystem.SYSTEM
}
}
// kotlinx-io (experimental)
@OptIn(InternalPersistentCacheApi::class, ExperimentalKotlinxIoCache::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
fileSystem = KotlinxIoCacheFileSystem() // defaults to kotlinx.io.files.SystemFileSystem
}
}KotlinxIoCacheFileSystem tracks kotlinx-io's own Alpha-stability kotlinx.io.files package and may
change shape between minor versions of this library. Okio is the recommended backend for
production.
If you already configure Ktor's own HttpCache, plug this library's storage into it without the
PersistentCache plugin. Each backend module exposes one factory that returns Ktor's CacheStorage
and needs no @InternalPersistentCacheApi opt-in:
import io.github.santimattius.persistent.cache.OkioCacheStorage
import io.ktor.client.plugins.cache.HttpCache
val client = HttpClient(CIO) {
install(HttpCache) {
publicStorage(OkioCacheStorage(maxSize = 20L * 1024 * 1024)) // or privateStorage(...)
}
}| Factory | Module | Stability |
|---|---|---|
OkioCacheStorage(...) |
cache-okio |
Stable |
KotlinxIoCacheStorage(...) |
cache-kotlinx-io |
Experimental, needs @OptIn(ExperimentalKotlinxIoCache::class)
|
Both take the same named parameters, with the same defaults as PersistentCacheConfig:
directory = "http_cache", maxSize = 10 MiB, ttl = 1 hour,
directoryProvider = getCacheDirectoryProvider(), the backend file system
(okio.FileSystem.SYSTEM / SystemFileSystem) and clock. They behave exactly like
install(PersistentCache) (atomic writes, TTL, LRU eviction; see Storage behavior).
| Prefer | When |
|---|---|
install(PersistentCache) |
You want the shortest setup. It also wires shared/public and routes the storage to publicStorage or privateStorage for you. |
| The factory | You want full control over HttpCache: several storages, a custom isShared, your own plugin ordering. Ktor's routing rules (Cache-Control: private goes to privateStorage) are then yours to configure. |
Use one storage instance per cache directory.
To control where the cache is stored (a custom folder, a 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) {
fileSystem = OkioCacheFileSystem()
directoryProvider = customProvider
}
}Without a custom provider, getCacheDirectoryProvider() returns the platform implementation
(Android app cache dir, iOS caches dir or JVM temp dir). cacheDirectory is a plain String, not
an okio.Path — see the migration guide if you are upgrading a pre-1.2 custom
provider.
This library installs Ktor's HttpCache and does not intercept the Auth pipeline. When you also
install Auth, behavior follows Ktor's cache routing:
Cache-Control: private (for example private, max-age=3600) to
be stored in private storage — the 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 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.These rules are verified by AuthPluginInteropTest in cache-okio, against the real
install(PersistentCache) DSL.
Which responses are persisted, and for how long?
This library persists whatever Ktor's HttpCache stores. Freshness comes from the origin server's
headers per RFC 7234 (Cache-Control: max-age=...,
Expires). Responses marked no-store are not written to disk.
What does ttl do, then?
It bounds how long an entry is retained on disk. Ktor's HttpCache already enforces HTTP freshness
from CachedResponseData.expires. See Storage behavior.
What happens if the app crashes mid-write?
Entries are written to a temporary file and moved into place atomically, so a crash cannot leave a
truncated entry. Stale *.tmp files are removed on startup and by clear().
Can two clients share one cache directory? Use one storage instance per directory. Concurrency guarantees are described in Storage behavior.
Does upgrading invalidate my cache?
From 2.0.0-alpha02, entry file names and format changed, and files written by earlier versions are
removed at startup. The only effect is cache misses while the cache refills. Switching between the
Okio and kotlinx-io backends does not invalidate the cache.
Four published modules:
| Module | Artifact | Contains |
|---|---|---|
cache-core |
ktor-persistent-cache-core |
The install(PersistentCache) DSL, the shared caching engine (FileCacheStorage), the CacheFileSystem backend SPI and CacheDirectoryProvider. Zero I/O dependencies. |
cache-okio |
ktor-persistent-cache-okio |
OkioCacheFileSystem and OkioCacheStorage — the default, stable backend. |
cache-kotlinx-io |
ktor-persistent-cache-kotlinx-io |
KotlinxIoCacheFileSystem and KotlinxIoCacheStorage — the experimental, opt-in backend. |
shared |
ktor-persistent-cache |
A facade depending on cache-core + cache-okio, keeping the pre-1.2 Maven coordinate working. |
Unpublished modules: cache-test-suite (shared conformance tests), sample (a KMP sample
verifying persistence with both backends), androidApp and iosApp (native shells for the sample).
| Document | Read it to |
|---|---|
| Storage behavior | Understand the on-disk layout, eviction, concurrency and ttl vs. expires. |
| Migration guide | Upgrade from a pre-1.2 version. |
| CHANGELOG | See what changed in each release. |
| Publishing | Publish to Maven Central (maintainers). |
| Ktor — Caching | Learn the HttpCache plugin this library builds on. |
./gradlew build # build all modules
./gradlew check apiCheck # tests and public API checks
./gradlew publishToMavenLocal # then use mavenLocal() in your projectThe unpublished :sample module has native Android and iOS shells. Each has independent Okio and
kotlinx-io actions that run an offline MockEngine persistence scenario.
androidApp in Android Studio and run the androidApp configuration, or run
./gradlew :androidApp:installDebug with a connected device.iosApp/iosApp.xcodeproj in Xcode and run the iosApp scheme. The build phase
compiles the KtorPersistentCacheSample framework from :sample automatically.Pass means the first client made one offline engine request, while a fresh second client made zero engine requests and returned the persisted response — real filesystem persistence, with no network dependency.
Contributions are welcome. Read CONTRIBUTING.md before opening an issue or pull request, and follow the Code of Conduct. To report a vulnerability, see SECURITY.md; for usage questions, see SUPPORT.md.
Licensed under the Apache License, Version 2.0.
Persistent HTTP caching for Ktor HttpClient on Kotlin Multiplatform.
Responses cached by Ktor's HttpCache are stored on
disk, so they survive app restarts. You choose the storage backend —
Okio (stable, default) or
kotlinx-io (experimental).
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
fileSystem = OkioCacheFileSystem()
}
}Status:
2.0.0-alpha02is a pre-release. The API may still change before2.0.0; see the CHANGELOG. Upgrading from a pre-1.2 version? Read the migration guide first — one change (a customCacheDirectoryProvider) needs a manual rewrite.
HttpCache factories,
custom directory, Auth
1. Add the dependency (plus a Ktor engine per target):
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.santimattius:ktor-persistent-cache:2.0.0-alpha02")
}
}
}2. Install the plugin:
import io.github.santimattius.persistent.cache.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
directory = "http_cache"
maxSize = 10L * 1024 * 1024 // 10 MB
ttl = 60L * 60 * 1000 // 1 hour
fileSystem = OkioCacheFileSystem()
}
}3. Use the client as usual:
val body: String = client.get("https://example.com/api/data").body()4. On Android only, make sure the library can resolve the application context — see Platform setup.
Notes:
fileSystem is required. cache-core ships no I/O dependencies by design, so the backend is
explicit.fileSystem and directoryProvider are typed against a backend SPI annotated
@InternalPersistentCacheApi. That is an intentional signal that the SPI is not a
stability-guaranteed surface, hence the @OptIn where you call install(PersistentCache).
If you would rather avoid the opt-in, use the
HttpCache factories.| Feature | What you get |
|---|---|
| Persistent storage | Cached responses live on the filesystem and survive app restarts. |
| Kotlin Multiplatform | One API for Android, iOS and JVM. |
| Idiomatic Ktor plugin |
install(PersistentCache) { ... }, built on Ktor's own HttpCache. |
| Choice of backend |
cache-okio (stable) or cache-kotlinx-io (experimental). Both share one caching engine and produce identical on-disk files, so switching backends keeps the cache valid. |
| Configurable | TTL, max size, directory name, shared vs. unshared, public vs. private storage. |
Vary-aware |
Variants (for example by Accept-Language) are cached separately. |
| LRU eviction | Least-recently-used entries are removed once the size limit is exceeded. |
| Crash-safe writes | Entries are written atomically and carry a format version. See Storage behavior. |
| Custom location | Optional CacheDirectoryProvider for tests or special directories. |
| API stability gate | Every published module is checked by binary-compatibility-validator; unreviewed public API changes fail CI. |
| Platform | Cache directory | Requirement |
|---|---|---|
| Android | Application cache dir (context.cacheDir) |
minSdk 24+, JVM target 17 |
| iOS | App caches directory (NSCachesDirectory, inside the sandbox) |
iosArm64, iosSimulatorArm64, iosX64
|
| JVM | java.io.tmpdir/ktor-cache |
JDK 17+ |
Built and tested with Kotlin 2.4.20 and Ktor 3.6.0 (ktor-client-core). You also need a Ktor
engine for each target (CIO, OkHttp, Darwin, ...).
The library ships as four Maven artifacts. Pick one path.
ktor-persistent-cache 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:2.0.0-alpha02")
}
// Also add a Ktor engine for each target, e.g.:
// implementation("io.ktor:ktor-client-okhttp") // Android
// implementation("io.ktor:ktor-client-cio") // JVM
// implementation("io.ktor:ktor-client-darwin") // iOS
}Use this for the experimental cache-kotlinx-io backend, or for the smallest dependency surface.
dependencies {
commonMain.dependencies {
implementation("io.github.santimattius:ktor-persistent-cache-core:2.0.0-alpha02")
implementation("io.github.santimattius:ktor-persistent-cache-okio:2.0.0-alpha02")
// or, instead of the line above:
// implementation("io.github.santimattius:ktor-persistent-cache-kotlinx-io:2.0.0-alpha02")
}
}gradle/libs.versions.toml)[versions]
ktorPersistentCache = "2.0.0-alpha02"
[libraries]
ktor-persistent-cache = { group = "io.github.santimattius", name = "ktor-persistent-cache", version.ref = "ktorPersistentCache" }
ktor-persistent-cache-core = { group = "io.github.santimattius", name = "ktor-persistent-cache-core", version.ref = "ktorPersistentCache" }
ktor-persistent-cache-okio = { group = "io.github.santimattius", name = "ktor-persistent-cache-okio", version.ref = "ktorPersistentCache" }
ktor-persistent-cache-kotlinx-io = { group = "io.github.santimattius", name = "ktor-persistent-cache-kotlinx-io", version.ref = "ktorPersistentCache" }Artifacts are published to Maven Central:
repositories {
mavenCentral()
}| Platform | Setup |
|---|---|
| Android | Needs the application context — see below. |
| iOS | None. Uses the default app caches directory. |
| JVM | None. Uses a subdirectory of the JVM temp directory. |
The library needs the application context to resolve the cache directory. The recommended way is App Startup:
ktor-persistent-cache must merge
the library's AndroidManifest, so the App Startup InitializationProvider and
ContextInitializer (in cache-core) are registered.ContextInitializer runs at app startup and injects the application context.
getCacheDirectoryProvider()
then uses it automatically.If you do not use the library's manifest (for example you use a different DI or startup path), call this 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 public API in io.github.santimattius.persistent.cache.startup. It throws
IllegalArgumentException if given a context that can leak memory, such as an Activity.
Options of PersistentCacheConfig, set inside install(PersistentCache) { ... }:
| Property | Type | Default | Description |
|---|---|---|---|
fileSystem |
CacheFileSystem<*>? |
null |
Required. The storage backend, e.g. OkioCacheFileSystem() or KotlinxIoCacheFileSystem(). @InternalPersistentCacheApi. |
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. <= 0 means no limit. |
ttl |
Long |
1 hour | Time-to-live for entries, in milliseconds. <= 0 means entries never expire. |
shared |
Boolean |
true |
Whether the cache is shared across requests (Ktor HttpCache behavior). |
public |
Boolean |
false |
When true, responses are treated as public (shareable across users); when false, private to the client. |
directoryProvider |
CacheDirectoryProvider? |
null |
Optional custom cache root; defaults to the platform getCacheDirectoryProvider(). @InternalPersistentCacheApi. |
clock |
() -> Long |
getTimeMillis() |
Supplies the current time; override for deterministic tests. |
There is no enabled toggle: to disable caching, omit install(PersistentCache).
Both backends implement the same CacheFileSystem<P> contract, pass the same conformance test
suite, and write byte-identical file names (SHA-256-based keys).
cache-okio |
cache-kotlinx-io |
|
|---|---|---|
| Stability | Stable (default) | Experimental |
Pulled in by ktor-persistent-cache
|
Yes | No — add the artifact (Path B) |
| Extra opt-in | None |
ExperimentalKotlinxIoCache (WARNING level) |
| Choose it when | Almost always | You already depend on kotlinx-io and want to avoid Okio |
// Okio (default)
@OptIn(InternalPersistentCacheApi::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
fileSystem = OkioCacheFileSystem() // defaults to okio.FileSystem.SYSTEM
}
}
// kotlinx-io (experimental)
@OptIn(InternalPersistentCacheApi::class, ExperimentalKotlinxIoCache::class)
val client = HttpClient(CIO) {
install(PersistentCache) {
fileSystem = KotlinxIoCacheFileSystem() // defaults to kotlinx.io.files.SystemFileSystem
}
}KotlinxIoCacheFileSystem tracks kotlinx-io's own Alpha-stability kotlinx.io.files package and may
change shape between minor versions of this library. Okio is the recommended backend for
production.
If you already configure Ktor's own HttpCache, plug this library's storage into it without the
PersistentCache plugin. Each backend module exposes one factory that returns Ktor's CacheStorage
and needs no @InternalPersistentCacheApi opt-in:
import io.github.santimattius.persistent.cache.OkioCacheStorage
import io.ktor.client.plugins.cache.HttpCache
val client = HttpClient(CIO) {
install(HttpCache) {
publicStorage(OkioCacheStorage(maxSize = 20L * 1024 * 1024)) // or privateStorage(...)
}
}| Factory | Module | Stability |
|---|---|---|
OkioCacheStorage(...) |
cache-okio |
Stable |
KotlinxIoCacheStorage(...) |
cache-kotlinx-io |
Experimental, needs @OptIn(ExperimentalKotlinxIoCache::class)
|
Both take the same named parameters, with the same defaults as PersistentCacheConfig:
directory = "http_cache", maxSize = 10 MiB, ttl = 1 hour,
directoryProvider = getCacheDirectoryProvider(), the backend file system
(okio.FileSystem.SYSTEM / SystemFileSystem) and clock. They behave exactly like
install(PersistentCache) (atomic writes, TTL, LRU eviction; see Storage behavior).
| Prefer | When |
|---|---|
install(PersistentCache) |
You want the shortest setup. It also wires shared/public and routes the storage to publicStorage or privateStorage for you. |
| The factory | You want full control over HttpCache: several storages, a custom isShared, your own plugin ordering. Ktor's routing rules (Cache-Control: private goes to privateStorage) are then yours to configure. |
Use one storage instance per cache directory.
To control where the cache is stored (a custom folder, a 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) {
fileSystem = OkioCacheFileSystem()
directoryProvider = customProvider
}
}Without a custom provider, getCacheDirectoryProvider() returns the platform implementation
(Android app cache dir, iOS caches dir or JVM temp dir). cacheDirectory is a plain String, not
an okio.Path — see the migration guide if you are upgrading a pre-1.2 custom
provider.
This library installs Ktor's HttpCache and does not intercept the Auth pipeline. When you also
install Auth, behavior follows Ktor's cache routing:
Cache-Control: private (for example private, max-age=3600) to
be stored in private storage — the 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 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.These rules are verified by AuthPluginInteropTest in cache-okio, against the real
install(PersistentCache) DSL.
Which responses are persisted, and for how long?
This library persists whatever Ktor's HttpCache stores. Freshness comes from the origin server's
headers per RFC 7234 (Cache-Control: max-age=...,
Expires). Responses marked no-store are not written to disk.
What does ttl do, then?
It bounds how long an entry is retained on disk. Ktor's HttpCache already enforces HTTP freshness
from CachedResponseData.expires. See Storage behavior.
What happens if the app crashes mid-write?
Entries are written to a temporary file and moved into place atomically, so a crash cannot leave a
truncated entry. Stale *.tmp files are removed on startup and by clear().
Can two clients share one cache directory? Use one storage instance per directory. Concurrency guarantees are described in Storage behavior.
Does upgrading invalidate my cache?
From 2.0.0-alpha02, entry file names and format changed, and files written by earlier versions are
removed at startup. The only effect is cache misses while the cache refills. Switching between the
Okio and kotlinx-io backends does not invalidate the cache.
Four published modules:
| Module | Artifact | Contains |
|---|---|---|
cache-core |
ktor-persistent-cache-core |
The install(PersistentCache) DSL, the shared caching engine (FileCacheStorage), the CacheFileSystem backend SPI and CacheDirectoryProvider. Zero I/O dependencies. |
cache-okio |
ktor-persistent-cache-okio |
OkioCacheFileSystem and OkioCacheStorage — the default, stable backend. |
cache-kotlinx-io |
ktor-persistent-cache-kotlinx-io |
KotlinxIoCacheFileSystem and KotlinxIoCacheStorage — the experimental, opt-in backend. |
shared |
ktor-persistent-cache |
A facade depending on cache-core + cache-okio, keeping the pre-1.2 Maven coordinate working. |
Unpublished modules: cache-test-suite (shared conformance tests), sample (a KMP sample
verifying persistence with both backends), androidApp and iosApp (native shells for the sample).
| Document | Read it to |
|---|---|
| Storage behavior | Understand the on-disk layout, eviction, concurrency and ttl vs. expires. |
| Migration guide | Upgrade from a pre-1.2 version. |
| CHANGELOG | See what changed in each release. |
| Publishing | Publish to Maven Central (maintainers). |
| Ktor — Caching | Learn the HttpCache plugin this library builds on. |
./gradlew build # build all modules
./gradlew check apiCheck # tests and public API checks
./gradlew publishToMavenLocal # then use mavenLocal() in your projectThe unpublished :sample module has native Android and iOS shells. Each has independent Okio and
kotlinx-io actions that run an offline MockEngine persistence scenario.
androidApp in Android Studio and run the androidApp configuration, or run
./gradlew :androidApp:installDebug with a connected device.iosApp/iosApp.xcodeproj in Xcode and run the iosApp scheme. The build phase
compiles the KtorPersistentCacheSample framework from :sample automatically.Pass means the first client made one offline engine request, while a fresh second client made zero engine requests and returned the persisted response — real filesystem persistence, with no network dependency.
Contributions are welcome. Read CONTRIBUTING.md before opening an issue or pull request, and follow the Code of Conduct. To report a vulnerability, see SECURITY.md; for usage questions, see SUPPORT.md.
Licensed under the Apache License, Version 2.0.