The Valkey Swift Package: from Local to Serverless

👍 0
Published: October 20, 2025
Written by:
Natan Rolnik
Natan Rolnik
👍 0

Server-side applications, especially ones that run on multiple servers, often need to share data temporarily. For example, storing expensive computation results, or a user identity for authentication, are common use cases. As the memory is not shared between servers, this data needs to be stored in a shared location. In stateless environments, such as serverless functions, this is even more important, as executions live for the duration of the request itself.

In the world of backend development, the most popular solution for this is Redis, a super-fast in-memory key-value store. However, in March 2024, Redis changed the license to a more restrictive one, making it no longer free for commercial use. Following that, the open-source community created a fork of Redis 7.2, the last version free to use. They called it Valkey, and the goal was to preserve a fully open, community-governed alternative to Redis.

Adam Fowler, member of the Swift Server Workgroup and one of the core contributors of Hummingbird, has recently implemented a Swift client for Valkey. In this post, you’ll learn:

  • how to run a Valkey server in a local Docker container and connect to it from your Swift code
  • how to deploy Valkey using AWS ElastiCache in serverless mode, and use it from a Lambda function
  • some of the capabilities of the Valkey Swift package, including pipeline operations and how to use the client to connect to a Valkey server

Warning: live examples ahead!

Basic Concepts

A simple analogy to understand how Valkey works is to think of it as a mix of a database and a key-value dictionary. To play with some of its commands, the official documentation provides an in-browser playground.

You can see some of the commands in the video below:

  • SET a value, and then GET it back
  • DEL to delete it
  • INCR to increment the count of a value by 1
  • SCAN to iterate over existing keys (possibly matching a pattern)

Tip: One useful feature of Valkey values is that you can set a TTL (Time To Live) to a value, so it will be automatically deleted after a specific amount of time. This is useful for caching, where data might be stale and irrelevant after a certain period.

All commands are documented in the Command Reference section of the official documentation.

Running the Valkey Docker Image

To test Valkey commands in a local environment, you can pull the official Valkey Docker image and run it locally.

To pull and run the image, you can use the commands below:

docker pull valkey/valkey:latest
docker run --name my-valkey -p 6379:6379 valkey/valkey

This will pull the latest Valkey image and start a server on port 6379, the default port for Redis/Valkey. It’s recommended to give a name to the container, so you can easily identify it later. If you want to run a detached container, in the background, you can add the -d flag (before the image name).

In the next sections, you’ll learn how to use the Valkey Swift package to connect to the local server, and also to an Elasticache instance.

Connecting to a Local Valkey Server

It’s time to use the Valkey Swift package to connect to the local image. In an existing Swift executable, start by adding the package dependency:

dependencies: [
    .package(url: "https://github.com/valkey-io/valkey-swift.git", from: "0.4.0"),
]

Then, add the product to the executable target dependencies:

targets: [
    .executableTarget(
        name: "my-app",
        dependencies: [
            .product(name: "Valkey", package: "valkey-swift"),
        ]
    )
]

To connect to the local server, initialize the Valkey client with a configuration whose host is set to localhost, and no TLS is needed:

import Valkey

// In your main function or your entrypoint
let valkeyClient = ValkeyClient(
    .hostname("localhost"),
    configuration: .init(
        tls: .disable,
    ),
    logger: .init(label: "ValkeyClient")
)

After the client is initialized, you must start a background task to manage the connection pool to the server. You can do this by calling the run() method using the async let syntax. This will keep the connection pool alive and will automatically reconnect to the server if the connection is lost.

// Don't forget starting the connection pool! 
// Otherwise, all commands will halt and wait
async let _ = valkeyClient.run()

Note: the run() method cannot be called more than once per process.

Using Valkey Commands

The Valkey client, using code generation, wraps all the commands available in Valkey. Here we’ll cover some of the basic ones.

Setting/Getting a Value

To set a value, you can use the .set method:

try await valkeyClient.set(
    "pokemon",
    value: ["Pikachu", "Charmander", "Bulbasaur"].randomElement()!
)

The set method also has some optional parameters, such as expiration, which allows passing a number of seconds or a Date object to set the TTL (Time To Live) of the value:

// previous parameters...
expiration: .seconds(60 * 60) // 1 hour

// or passing a Date:
expiration: Date().addingTimeInterval(60 * 60 * 24) // 1 day from now

Incrementing a Counter

Incrementing a counter is a very common operation, and the client provides a .incr method to do so:

try await valkeyClient.incr("pokemon-count")

This will increment the value of the key pokemon-count by 1. If the key doesn’t exist, it will be created with a value of 1.

To make this post more interesting, we configured a simple counter that displays the number of readers who have reached this point in the post:

Number of readers who have visited this page:
Loading...

Pipeline Operations

Another powerful feature of Valkey that the Swift package supports is pipelining operations: it allows performing multiple commands in a single request. This is useful because it reduces the number of round trips to the server, improving the performance of both the server and the client.

A visual representation of a Valkey pipeline operation, in Adam's talk at ServerSide.swift conference A visual representation of a Valkey pipeline operation, in Adam's talk at ServerSide.swift conference
A visual representation of a Valkey pipeline operation, in Adam's talk at ServerSide.swift conference

The pipeline execute method uses Swift’s new parameter pack syntax to pass multiple commands and return a tuple with the results of each command. For example, here we’ll increment three different counters in a single request:

let (totalCount, _, _) = await valkey.execute(
    INCR("stats:total"),
    INCR(ValkeyKey("stats:os:(stats.os)")),
    INCR(ValkeyKey("stats:browser:(stats.browser)"))
)

In this example, we’re incrementing three different counters:

  • stats:total
  • stats:os:<os>
  • stats:browser:<browser>

The execute method returns a tuple with the results of each command, each as a Result<Int, Error>. In this case, the Success value of all three commands is an Int, but they could be of different types depending on the command.

Because the code above is only interested in one of the results, it uses the underscore _ as a placeholder, ignoring the other two results. totalCount is a Result<Int, Error>, which contains the final value of the total counter, and can be accessed using the Result.get method to extract the value.

The graph below is calculated using the numbers added by the operation above, and updated every time a new visitor reaches this section of the post:

Loading statistics...

Deploying with SwiftCloud

The sample project of this post uses the counter and the stats in a Lambda function, accessing the Valkey server via ElastiCache. To deploy it, the magical SwiftCloud package (maintained by Andrew Barba), comes to the rescue.

If you’re not familiar with SwiftCloud, you can learn more about it in our previous posts about it.

The most basic setup for a Lambda function to access Valkey requires a VPC (Virtual Private Cloud) to allow the Lambda function, which is public to the internet, to access the Valkey server, which is not exposed publicly.

Luckily, SwiftCloud takes care of all the infrastructure setup for you, including the VPC and the Security Group.

To start, after having configured SwiftCloud and its Infra target in your Package.swift file, define its main entry point and the build function:

import CloudAWS

@main
struct Infra: AWSProject {
    func build() async throws -> Outputs {
        // implementation below...
    }
}

Then, add a VPC that will be used by both Lambda and ElastiCache. The Lambda should be configured in the public subnet on that same VPC, so it can be accessed from the internet.

let vpc = AWS.VPC("my-vpc")

let lambda = AWS.Function(
    "my-counter",
    targetName: "<your-lambda-target>",
    url: .enabled(),
    vpc: .public(vpc)
)

Then, add the ElastiCache resource:

let cache = AWS.Cache(
    "my-valkey",
    vpc: .private(vpc)
)

By default, SwiftCloud chooses the Valkey engine for the ElastiCache resource, so the lines above are all you need to create it in the private subnet of the VPC.

Finally, link the Lambda to ElastiCache and return the URL of the Lambda as the output of the build function:

lambda.link(cache)

return [
    "Function URL": lambda.url
]

To deploy the project, run the Infra target and its deploy command:

swift run Infra deploy --stage dev

Accessing ElastiCache from a Lambda Function

While in the previous sections we showed how to connect to a local Valkey server, connecting to ElastiCache requires two changes in the client initialization:

  1. Using the hostname of the ElastiCache instance, instead of the local hostname
  2. Passing a TLS configuration to the Valkey client, with the tlsServerName parameter set to the same hostname of the ElastiCache instance (from the item above)

SwiftCloud automatically sets the environment variables for you and provides type-safe Swift code to access them under the Cloud.Resource namespace:

let valkeyClient = try ValkeyClient(
    .hostname(Cloud.Resource.MyValkey.hostname),
    configuration: .init(
        tls: .enable(.clientDefault, tlsServerName: Cloud.Resource.MyValkey.hostname),
    ),
    logger: .init(label: "ValkeyClient")
)

After that, you use the run() method to start the connection, and you’re ready to use the Valkey client from your Lambda function 🚀🥳

Explore Further

If you want to learn more about the Valkey Swift package, don’t miss the introductory talk by Adam Fowler at the ServerSide.swift conference in London.

If you want to contribute to the Valkey Swift package, or are curious about the implementation details, you can find the repository here. The package is currently in a pre-release status, so now is the time to provide feedback on the API!

Huge thanks to Adam for reviewing the post before publishing and for writing the package itself, and to Andrew for helping with the deployment, from getting the Elasticache setup right, to the latest fixes in Swift Cloud.

Let us know if you have any questions or comments, at X or Mastodon.

See you at the next post. Have a good one!
Swift, Xcode, the Swift Package Manager, iOS, macOS, watchOS and Mac are trademarks of Apple Inc., registered in the U.S. and other countries.

© Swift Toolkit, 2024-2025