SPTPersistentCache: an LRU cache for iOS that keeps one file per entry
Everyone tries to implement a cache at some point in their iOS app’s lifecycle, and this is ours.
At a glance
- What is it?
- Spotify's SPTPersistentCache is a disk cache where every entry is its own file with a header recording the TTL, the last updated time, and a redundancy check, so reads and writes of different entries never block each other. It adds a locking mechanism and a scheduled garbage collector on top of the LRU behaviour.
- Who is it for?
- Adopt SPTPersistentCache if you need an on-disk cache in an Objective-C or Swift iOS app where entries have different lifetimes and you want to pin some of them. Do not adopt it for a small in-memory cache, since the per-file design trades storage overhead for write concurrency.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 99 days ago.
- What is it written in?
- Mainly Objective-C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One file per entry, with a header on each
SPTPersistentCache is described as a library that allows people to cache NSData with time to live values and semantics for disk management, and the project frames it plainly: everyone tries to implement a cache at some point in their app's lifecycle, and this is Spotify's.
The architecture is an LRU cache that makes use of the file system to store files, and it inserts a cache header into each file. That header is what makes the design work. It tracks the TTL, the last updated time, the redundancy check, and more, which allows the cache to know how often a file is accessed, when it was made, whether it has become corrupt, and whether the cache is stale.
Using different files rather than a single binary file allows multiple reads and writes on different files within the cache without complicated blocking logic. That is the trade the design makes. A single file would be compact and easy to index, but every write would have to serialise against every other write, and in an app that caches images fetched from a network, concurrent writes are the normal case rather than the exception.
The per-file layout has a second consequence, which the documentation mentions in passing. The cache header in each file can be removed quite easily, and this is visible in the SPTPersistentCacheViewer tool, which is made to run on OS X. The tool is a debugging aid, and its existence tells you the author expected people to want to look inside the cache.
The project is Apache-2.0 licensed, the primary language is Objective-C, and the last push to the repository was on 2026-07-15. The releases are 1.2.0 from that date, 1.1.2 from 2024-04-20, and 1.1.1 from 2017-08-15, and the repository also carries a Package.swift, so the library is consumable from Swift as well as from Objective-C.
Creating a cache, and the one mistake to avoid
The creation code shows the options object, and the values in the example are the interesting part because each one maps to a policy decision.
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingString:@"com.spotify.demo.image.cache"];
SPTPersistentCacheOptions *options = [SPTPersistentCacheOptions new];
options.cachePath = cachePath;
options.cacheIdentifier = @"com.spotify.demo.image.cache";
options.defaultExpirationPeriod = 60 * 60 * 24 * 30; // 30 days
options.garbageCollectionInterval = (NSUInteger)(1.5 * SPTPersistentCacheDefaultGCIntervalSec);
options.sizeConstraintBytes = 1024 * 1024 * 10; // 10 MiB
options.debugOutput = ^(NSString *string) {
NSLog(@"%@", string);
};
SPTPersistentCache *cache = [[SPTPersistentCache alloc] initWithOptions:options];The cache path is inside the Caches directory, which is the right place for something the system may purge under pressure. The default expiration period is thirty days. The size constraint is ten mebibytes, which is a hard cap the cache will respect. The garbage collection interval is expressed as a multiple of the library's default constant, so the default is the unit of account.
The guidance about instances is the part worth getting right. It is best to use different caches for different types of data, and not one big cache for the entire application. However, only create one SPTPersistentCache instance for each cache, otherwise you might encounter anomalies when two different caches end up writing to the same file.
That is a sharp edge. The cache is identified by a path and a cache identifier, and two instances pointed at the same directory are two writers to the same files, with two sets of LRU bookkeeping and two garbage collectors. The library cannot prevent it, so the API documentation has to.
Keys are filenames, and hashing is your job
Storing data requires one decision that catches people out: the key is used as the file name within the cache directory, and the library deliberately does not hash it for you.
The reason is given explicitly. The authors did not implement a hash function under the hood because they wanted to give the option of what hash function to use to the user, so it is recommended that when you insert data into the cache for a key, you create the key using your own hashing function. At Spotify they use SHA1, although better hashing functions exist these days.
The reason to prefer your own hash is worth stating plainly, because it is not obvious from the API. A key is a path component, and path components have rules about length, about which characters are legal, and about collisions. A hex digest from a cryptographic hash is a fixed length, is composed of safe characters, and has a negligible collision probability, and a library that picked one for you would be making a security decision on your behalf.
There is also a second way to store a value, and it changes the lifetime semantics. If you want the cache record, that is the file, to exist without any TTL, you make sure you store it as a locked file.
NSData *data = UIImagePNGRepresentation([UIImage imageNamed:@"my-image"]);
NSString *key = @"MyHashValue";
[self.cache storeData:data
forKey:key
locked:YES
withCallback:^(SPTPersistentCacheResponse *cacheResponse) {
NSLog(@"cacheResponse = %@", cacheResponse);
} onQueue:dispatch_get_main_queue()];Reading is the mirror image, with the same key handed back:
NSString *key = @"MyHashValue";
[self.cache loadDataForKey:key withCallback:^(SPTPersistentCacheResponse *cacheResponse) {
UIImage *image = [UIImage imageWithData:cacheResponse.record.data];
} onQueue:dispatch_get_main_queue()];And the note that matters for a caller: if the TTL has expired, you will not receive a result. The absence of a result is how expiry is reported, so a caller that treats a nil response as an error will misreport every expired entry as a failure.
Locking, and the contract you take on
The locking mechanism exists for a specific reason, and the example given is concrete. Sometimes you want to lock a file in SPTPersistentCache long after it has been expired by its TTL. An example of where Spotify does this is images relating to the cover art of songs you have offlined, because they would not want to invalidate those images when you are away on vacation.
NSString *key = @"MyHashValue";
[self.cache lockDataForKeys:@[key] callback:nil queue:nil];
// Now my data is safe within the arms of the cache
[self.cache unlockDataForKeys:@[key] callback:nil queue:nil];
// Now my data will be subject to its original TTLThe warning is stated plainly: when you have locked a file, you must make sure you unlock it eventually, otherwise it will stay around forever. A lock is described as a contract between the cache and its consumer that the data will remain in the cache until it is explicitly unlocked.
That is a manual reference-counting scheme with no owner tracking, and it has the same failure mode as any such scheme: an entry that is locked and never unlocked lives forever, and the cache grows until the size constraint prunes it. There is a second constraint in the documentation that limits the damage: if you exceed the constrained size in your cache, even locked files can be subject to pruning.
So the guarantee is bounded. A locked file is protected from expiry-based collection, but not from a size-driven prune, which is the behaviour you would want on a device that is nearly out of space and where keeping a stale image is less important than being able to write a new one.
Garbage collection you have to schedule yourself
The garbage collection functionality in SPTPersistentCache is not automatically run, and you have to call it manually using scheduleGarbageCollector:
[self.cache scheduleGarbageCollection];This schedules garbage collection on the interval you supplied in the SPTPersistentCacheOptions class, which by default is the SPTPersistentCacheDefaultExpirationTimeSec external variable. To unschedule it you call the opposite function:
[self.cache unscheduleGarbageCollection];The library scheduling this itself would be the obvious convenience, and the fact that it does not is a deliberate choice with a plausible reason. The garbage collector is the component that decides what to delete, and a library deleting your data on a timer it chose, without the app knowing, is a behaviour that is hard to debug and easy to get wrong on a background thread. Making the app call it means the app decides when, and the schedule is an option the app sets explicitly.
The interval is expressed relative to a library constant, so the app picks a multiple of the default rather than an absolute number of seconds, and the constant is exposed as an external variable rather than a compile-time constant, which means it can be adjusted.
The README also describes manual cache management for the cases where you want to pre-empty a scheduled collection, wipe the cache, or remove all locked and unlocked files, and there is a separate viewer tool for OS X, plus a CHANGELOG.md, a podspec, a Package.swift, and a ci/ directory in the repository, so the library ships through CocoaPods, Swift Package Manager, and its own continuous integration.
Editorial conclusion
Adopt SPTPersistentCache if you need an on-disk cache in an Objective-C or Swift iOS app where entries have different lifetimes and you want to pin some of them. Do not adopt it for a small in-memory cache, since the per-file design trades storage overhead for write concurrency. Verify first that the API you need exists in the version you take, because the most recent release is 1.2.0 from July 2026 and the documented Podfile version is still 1.1.1.
Frequently asked questions
What is SPTPersistentCache and how is it built?
It is Spotify's LRU disk cache for iOS, storing NSData with time to live values. It uses the file system with a separate file per entry and inserts a cache header into each file, which tracks the TTL, the last updated time, and a redundancy check, so different entries can be read and written without complicated blocking logic.
Why does SPTPersistentCache use my key as the file name?
Because the authors did not want to impose a hash function on you, so the key becomes the file name and you are recommended to create it with your own hashing function. At Spotify they use SHA1, although the documentation notes that better hashing functions exist now.
What does locking a file in SPTPersistentCache do?
It protects an entry from being removed after its TTL expires, for example the cover art of songs you have offlined, so it is not invalidated while you are away. The lock is a contract that the data stays until it is explicitly unlocked, and if you never unlock it the entry stays forever, though even locked files can be pruned if the size constraint is exceeded.
Is garbage collection run automatically in SPTPersistentCache?
No. You have to call scheduleGarbageCollector yourself, and it then runs on the interval you set in STPersistentCacheOptions, by default the SPTPersistentCacheDefaultExpirationTimeSec external variable. You stop it with unscheduleGarbageCollection.
Which platforms and versions does SPTPersistentCache support?
The README lists iOS 8.0 and later, and OS X 10.10 and later. It is Apache-2.0 licensed, written in Objective-C with a Package.swift in the repository so it can be used from Swift, and the most recent release is 1.2.0 from 2026-07-15.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/spotify-sptpersistentcache)Was this useful?