• Dmitry Amelchenko

Expo FileSystem.cacheDirectory must be cleaned manually.

Updated: Jul 28, 2021

Everything that was written in this article originally was caused by a nasty bug in my application, which lead me to wrongly believe there is some problem with how FileSystem.cachDirectory works in Expo.

Before you proceed any further, consider reading the following article which explains the issue:


I have built a mobile app in Expo which is all about sharing photos ( it's very heavy on serving images, and it needs to do it super fast. Since react-native-fast-image is not available in Expo managed workflow, I had to implement my own caching solution, which worked extremely well at first, but then... my app started to crash!!!

I spent days chasing the issue, and the only thing I could link it to was the Expo's FileSystem.cacheDirectory.

This is especially said, because I always assumed, that device's OS has to take care of maintaining the right balance between the amounts of info stored in cache folder and the health of the system.

By trial an error I found, that, when the app starts to crash eventually, the only way to get it back to working state is to re-install it from the store, after which it will work for awhile again, usually for couple of weeks, and then the cycle repeats. I can't expect my customers to re-install the app every time it starts to crash. The next time it started to happened again, I tried to wipe the cacheFolder via pushing over-the-Air update, instead of re-installing -- and it fixed it! Great -- I'm on the right track.

So, here is the dilemma -- I can't expect my customers to re-instal the app every couple of weeks, but I can't serve all the images without cache either. There has to be a compromise solution.

As the result I wrote a better version of the function which cleans up the cache folder. The function is invoked on the app start, keeping up to 8k files that were most recently cached, removing the rest.

Here is the implementation:

export const cleanupCache = () => async (dispatch, getState) => {
  // _checkUploadDirectory()

  const cacheDirectory = await FileSystem.getInfoAsync(CONST.IMAGE_CACHE_FOLDER)
  // create cacheDir if does not exist
  if (!cacheDirectory.exists) {
    await FileSystem.makeDirectoryAsync(CONST.IMAGE_CACHE_FOLDER)

  if (Platform.OS === 'ios') {
    // cleanup old cached files
    const cachedFiles = await FileSystem.readDirectoryAsync(`${CONST.IMAGE_CACHE_FOLDER}`)

    let position = 0
    let results = []
    const batchSize = 10

    // batching promise.all to avoid exxessive promisses call
    while (position < cachedFiles.length) {
      const itemsForBatch = cachedFiles.slice(position, position + batchSize)
      results = [...results, ...await Promise.all( file => {// eslint-disable-line
        const info = await FileSystem.getInfoAsync(`${CONST.IMAGE_CACHE_FOLDER}${file}`)// eslint-disable-line
        return Promise.resolve({ file, modificationTime: info.modificationTime, size: info.size })
      position += batchSize

    // cleanup cache, leave only 5000 most recent files
    const sorted = results
      .sort((a, b) => a.modificationTime - b.modificationTime)

    for (let i = 0; sorted.length - i > 8000; i += 1) { // may need to reduce down to 500
      FileSystem.deleteAsync(`${CONST.IMAGE_CACHE_FOLDER}${sorted[i].file}`, { idempotent: true })



The implementation is pretty straight forward and self explanatory. To view the source, check it out in my git repo:

or expo slack:

The approach described here was used in the following npm module

Thanks for reading.


Recent Posts

See All

I'm building my killer mobile app on graphql. On the backend I'm using AWS AppSync -- I'm still not bought into the magic of Amplify -- there is just a way too much magic going on behind the scenes, w