Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

27. Caching
Written by Tanner Nelson

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

Whether you’re creating a JSON API, building an iOS app, or even designing the circuitry of a CPU, you’ll eventually need a cache. Caches (pronounced cashes) are a method of speeding up slow processes and, without them, the Internet would be a terribly slow place. The philosophy behind caching is simple: store the result of a slow process so you only have to run it once. Some examples of slow processes you may encounter while building a web app are:

  • Large database queries.
  • Requests to external services, e.g., other APIs.
  • Complex computation, e.g., parsing a large document.

By caching the results of these slow processes, you can make your app feel snappier and more responsive.

Cache storage

As part of DatabaseKit, Vapor defines the protocol KeyedCache. This protocol creates a common interface for different cache storage methods. The protocol itself is quite simple; take a look:

public protocol KeyedCache {
  // 1
  func get<D>(_ key: String, as decodable: D.Type) 
    -> Future<D?> where D: Decodable

  // 2
  func set<E>(_ key: String, to encodable: E) 
    -> Future<Void> where E: Encodable

  // 3
  func remove(_ key: String) -> Future<Void>
}

Here’s what each method does:

  1. get(_:as:) fetches stored data from the cache for a given key. If no data exists for that key, it returns nil.
  2. set(_:to:) stores data in the cache at the supplied key. If a value existed previously, it’s replaced.
  3. remove(_:): Removes data, if any, from the cache at the supplied key.

Each method returns a Future since interaction with the cache may happen asynchronously.

Now that you understand the concept of caching and the KeyedCache protocol, it’s time to take a look at some of the actual caching implementations available with Vapor.

In-memory caches

Vapor comes with two memory-based caches: MemoryKeyedCache and DictionaryKeyedCache. These caches store their data in your program’s running memory. This makes both of these caches great for development and testing because they have no external dependencies. However, they may not be perfect for all uses as the storage is cleared when the application restarts and can’t be shared between multiple instances of your application. Most likely though, this memory volatility won’t affect a well thought out caching design.

The differences between MemoryKeyedCache and DictionaryKeyedCache are subtle but important. Here’s a more in-depth look.

Memory cache

The contents of a MemoryKeyedCache are shared across all your application’s event loops. This means once something is stored in the cache, all future requests will see that same item regardless of which event loop they are assigned to. This is great for testing and development because it simulates how an external cache would operate. However, MemoryKeyedCache storage is still process-local, meaning it can not be shared across multiple instances of your application when scaling horizontally.

More importantly, the implementation of this cache is not thread safe and, thus, requires synchronized access. This makes MemoryKeyedCache unsuitable for use in production systems.

Dictionary cache

The contents of a DictionaryKeyedCache are local to each event loop. This means that subsequent requests assigned to different event loops may see different cached data. Separate instances of your application may also cache different data. This behavior is fine for purely performance-based caching, such as caching the result of a slow query, where logic does not rely on the cache storage being synchronized. However, for uses like session storage, where cache data must be synchronized, DictionaryKeyedCache will not work.

Because DictionaryKeyedCache does not share memory between event loops, it is suitable for use in production systems.

Database caches

All DatabaseKit-based caches support using a configured database as your cache storage. This includes all of Vapor’s Fluent mappings (FluentPostgreSQL, FluentMySQL, etc.) and database drivers (PostgreSQL, MySQL, Redis, etc.).

If you want your cached data to persist between restarts and be shareable between multiple instances of your application, storing it in a database is a great choice. If you already have a database configured for your application, it’s easy to set up.

You can use your application’s main database for caching or you can use a separate, specialized database. For example, it’s common to use a Redis database for caches.

Redis

Redis is an open-source, cache storage service. It’s used commonly as a cache database for web applications and is supported by most deployment services like Heroku. Redis databases are usually very easy to configure and they allow you to persist your cached data between application restarts and share the cache between multiple instances of your application. Redis is a great, fast and feature-rich alternative to in-memory caches and it only takes a little bit more work to configure.

Now that you know about the available caching implementations in Vapor, it’s time to add caching to an application.

Example: Pokédex

When building a web app, making requests to other APIs can introduce delays. If the API you’re communicating with is slow, it can make your API feel slow. Additionally, external APIs may enforce rate limits on the number of requests you can make to them in a given time period.

Fortunately, with caching, you can store the results of these external API queries locally and make your API feel much faster.

You’re going to use a cache to improve the performance of “Pokédex”, an API for storing and listing all Pokémon you’ve captured.

You’ve already learned how to create a basic CRUD API and how to make external HTTP requests. As a result, this chapter’s starter project already has the basics implemented.

In Terminal, change to the starter project’s directory and use the following command to generate and open an Xcode project to work in:

vapor xcode -y

Overview

This simple Pokédex API has two routes:

  • GET /pokemon: Returns a list of all captured Pokémon.
  • POST /pokemon: Stores a captured Pokémon in the Pokédex.

When you store a new Pokémon, the Pokédex API makes a call to the external API pokeapi.co to verify that the Pokémon name you’ve entered is real. While this check works, the pokeapi.co API can be pretty slow to respond, thereby making your app feel slow.

Normal request

A typical Vapor requests takes only a couple of milliseconds to respond, when working locally. In the screenshot that follows, you can see the GET /pokemon route has a total response time of about 40ms.

PokeAPI dependent request

In the screenshot below, you can see that the POST /pokemon route is 25x slower at around 1,500ms. This is because the pokeapi.co API can be slow to respond to the query.

Now you’re ready to take a look at the code to better understand what’s making this route slow and how a cache can fix it.

Verifying the name

In Xcode, open PokeAPI.swift and look at verifyName(_:on:).

This class is a simple wrapper around an HTTP client and makes querying the PokeAPI more convenient. It verifies the legitimacy of a supplied Pokémon name. If the name is real, the method returns true, wrapped in a Future.

Now look at fetchPokemon(named:). This method sends the request to the external pokeapi.co and returns the Pokémon’s data. If a Pokémon with the supplied name doesn’t exist, the API — and, therefore, this method — returns a 404 Not Found response.

fetchPokemon(named:) is the cause of the slow response time on the POST /pokemon route. A KeyedCache is just what the doctor ordered!

Creating a KeyedCache

The first task is to create a KeyedCache for the PokeAPI wrapper. In PokeAPI.swift, add a new property to store the cache below let client: Client:

let cache: KeyedCache

Next, replace the implementation of init to account for the new property:

public init(client: Client, cache: KeyedCache) {
  self.client = client
  self.cache = cache
}

Finally, fix the remaining compiler error by replacing the return statement in makeService(for:) with:

return try PokeAPI(
  client: container.make(),
  cache: container.make())

Build and run, then create a new request in RESTed. Configure the request as follows:

Add one parameter with name and value:

  • name: Test

You’ll see the following error:

[ ERROR ] ServiceError.ambiguity: Please choose which KeyedCache you prefer, multiple are available: MemoryKeyedCache, DictionaryKeyedCache, DatabaseKeyedCache<ConfiguredDatabase<SQLiteDatabase>>. (Config.swift:72)
[ DEBUG ] Suggested fixes for ServiceError.ambiguity: `config.prefer(MemoryKeyedCache.self, for: KeyedCache.self)`. `config.prefer(DictionaryKeyedCache.self, for: KeyedCache.self)`. `config.prefer(DatabaseKeyedCache<ConfiguredDatabase<SQLiteDatabase>>.self, for: KeyedCache.self)`. (Logger+LogError.swift:20)

This may look intimidating at first, but don’t worry, it’s expected. Since this application is configured to use FluentSQLite as its database, there are multiple KeyedCache implementations available. Since Fluent is already configured, you’ll use SQLiteCache (DatabaseKeyedCache<ConfiguredDatabase<SQLiteDatabase>>).

Open configure.swift and add following line before return migrations:

migrations.prepareCache(for: .sqlite)

Just as you have to run a migration to set up your models in the database, you must allow Fluent to configure the underlying database schema for storing cache data.

Next, add the following at the end of configure(_:_:_:):

config.prefer(SQLiteCache.self, for: KeyedCache.self)

This tells Vapor to use SQLite as your application’s KeyedCache. This resolves the ambiguity error.

Note: Fluent uses the table fluentcaches to store the cache data.

Build and run. Use RESTed to send the same request to POST /pokemon. You’ll now see the following in the Response Body:

{
    "error": true,
    "reason": "Invalid Pokemon Test."
}

Great! You’ve created your KeyedCache. Time to put it to work.

Fetch and Store

Now that the PokeAPI wrapper has access to a working KeyedCache, you can use the cache to store responses from the pokeapi.co API and subsequently fetch them much more quickly.

Open PokeAPI.swift and replace the implementation of verifyName(_:on:) with the following:

public func verifyName(_ name: String, on worker: Worker) 
    -> Future<Bool> {
  // 1
  let key = name.lowercased()
  // 2
  return cache.get(key, as: Bool.self).flatMap { result in
    if let exists = result {
      // 3
      return worker.eventLoop.newSucceededFuture(result: exists)
    }

    // 4
    return self.fetchPokemon(named: name).flatMap { res in
      switch res.http.status.code {
      case 200..<300:
        // 5
        return self.cache.set(key, to: true).transform(to: true)
      case 404:
        return self.cache.set(key, to: false)
          .transform(to: false)
      default:
        let reason = 
          "Unexpected PokeAPI response: \(res.http.status)"
        throw Abort(.internalServerError, reason: reason)
      }
    }
  }
}
  1. Create a consistent cache key by lowercasing the name. This ensures that both “Pikachu” and “pikachu” share the same cache result.
  2. Query the cache to see if it contains the desired result.
  3. If a cached result exists, return that result. This means that calls to verifyName(_:on:) will never invoke fetchPokemon(named:) a second time for a given name. This is the key step that will improve performance.
  4. When fetchPokemon(named:) completes, store the result of the API query in the cache.

Build and run.

Once again, use RESTed to send the same request to POST /pokemon . Take note of the response time for the first request. It’ll likely be a couple of seconds. Now, make a second request and note the time; it should be much faster!

Where to go from here?

Caching is an important concept in Computer Science and understanding how to use it will help make your web applications feel fast and responsive. There are several methods for storing your cache data for web applications: in-memory, Fluent database, Redis and more. Each has distinct benefits over the other.

You can checkout the different types of algorithms available for caching such as Least Recently Used (LRU), Random Replacement (RR) or Last In First Out (LIFO). Each of these has pros and cons depending on the type of application you’re writing and the type of data you’re caching within it.

In this chapter, you learned how to configure a Fluent database cache. Using the cache to save the results of a request to an external API, you significantly increased the responsiveness of your app.

If you’d like a challenge, try configuring your app to use a Redis or in-memory cache instead of the SQLiteCache. But remember, you gotta cache ’em all!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.