Today’s Swift services often operate on the same cloud-native platforms that power much of the modern Kubernetes ecosystem—including ConfigMaps, containerized applications, declarative deployments, and service lifecycle management. Projects like Prometheus and OpenTelemetry have established common standards for observability and operations across distributed systems, yet configuration handling in Swift services has remained largely informal.
Swift is increasingly used to build production-grade services on Linux, leveraging its modern concurrency model, memory and data-safety guarantees, and high performance. Yet in practice, developers often build configuration systems by directly reading environment variables with ProcessInfo.environment or parsing configuration files in YAML, JSON, or similar formats.
While these methods work for basic scenarios, they fail to solve several critical operational challenges:
- There’s no standard approach for combining multiple configuration sources with clear priority rules.
- Reloading configuration from a ConfigMap-mounted volume can lead to partial reads during active traffic.
- A single request may see an inconsistent view of configuration if an update occurs partway through
Swift Configuration was designed to fill these gaps. It offers a layered provider system with explicit precedence rules, file-based reloading tailored for Kubernetes-style ConfigMap volumes, and immutable configuration snapshots that ensure all consumers see a stable view of settings during runtime changes.
This article walks through these patterns using a complete Kubernetes service as a practical example.
Reading configuration: consumers, providers, and hierarchy
The Swift Configuration library cleanly separates reading configuration from providing it. A ConfigReader accepts an ordered list of types that implement ConfigProvider. For any given key, the first provider that contains a value takes priority. You define the precedence chain explicitly.
In production deployments, it’s typical to stack providers with the most specific sources first:
// Providers initialize asynchronously: EnvironmentVariablesProvider reads
// the process environment and .env file at startup, so initialization is async.
async let staticProviders: [(any ConfigProvider)] = [
CommandLineArgumentsProvider(),
EnvironmentVariablesProvider(),
EnvironmentVariablesProvider(environmentFilePath: ".env", allowMissing: true),
InMemoryProvider(values: [
"log.level": "info",
"config.filePath": "/etc/config/appsettings.yaml",
"config.pollIntervalSeconds": 15,
"http.serverName": "my-service",
]),
]Here, command-line arguments take priority over environment variables, which override a .env file, with in-memory defaults serving as fallback. The order clearly defines the hierarchy. There’s no hidden behavior—adding or rearranging sources requires just a single-line change.
Pass one or more providers to a ConfigReader, which you then use to access values:
let initConfig = ConfigReader(providers: staticProviders)You retrieve values from the configuration provider like this:
let logLevel = initConfig.string(
forKey: "log.level",
as: Logger.Level.self,
default: .info
)Configuration keys in Swift Configuration use dot notation to represent logical groupings. Scoped readers allow individual components to access a subset of the configuration without needing the full key path:
let httpConfig = initConfig.scoped(to: "http") // reads "http.port", "http.host" as just "port", "host"For providers that don natively support dots—such as environment variables and .env files—dot-notation keys are converted automatically. For example, the key `log.level` maps to the environment variable `LOG_LEVEL`. The same key works seamlessly across all provider types without manual translation.
Dynamic reloading from a ConfigMap
Static providers work well for bootstrap configuration that doesn’t change, but some settings need to be updated while the service runs. For values that must change without restart—like feature flags, rate limits, or connection pool sizes—use a dynamic provider.
ReloadingFileProvider is a built-in provider that watches a file for modifications and delivers consistent snapshots with each update. In Kubernetes, mount a ConfigMap as a volume and configure the provider to watch that mounted path. Kubernetes manages the file updates; the ReloadingFileProvider handles the reloading logic.
Swift Configuration includes built-in YAML and JSON readers. Anyone can implement ConfigProvider to support additional formats—for instance, the community has already created a TOML reader. The following setup shows a YAML-based reloading provider that reads its file path from the static configuration:
let reloadingProvider = try await ReloadingFileProvider<yamlsnapshot>(
config: initConfig.scoped(to: "config")
)With the provider scoped to the `config` key, it reads `config.filePath` and `config.pollIntervalSeconds` from the initial ConfigReader.
Now create a combined configuration reader with the dynamic provider at the highest priority, ready to use across your service:
let config = ConfigReader(
providers: [reloadingProvider] + staticProviders
)On the Kubernetes side, your configuration lives in a ConfigMap. The YAML structure should mirror what your code expects—nested keys align with the dot-notation paths used in the ConfigReader:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: my-service-config
data:
appsettings.yaml: |
app:
name: "production"The Deployment manifest connects the ConfigMap volume to the container and specifies the file path that the provider will monitor. You’ll find the full manifest in the accompanying repository example:
containers:
- name: http-server
image: reloading-example:latest
env:
- name: LOG_LEVEL
value: "debug"
# path to the configuration file mounted from ConfigMap
- name: CONFIG_FILE_PATH
value: "/etc/config/appsettings.yaml"
volumeMounts:
-
name: config-volume
When you apply an updated ConfigMap using kubectl apply, Kubernetes pushes the changes to the mounted volume. This process usually takes a minute or two, depending on the kubelet sync cycle and the cluster’s ConfigMap cache TTL. The polling interval determines how fast the provider notices a file update, but it doesn’t shorten the time Kubernetes takes to propagate the change. By default, the interval is set to 15 seconds—frequent enough to catch changes quickly once they appear, without overloading the filesystem.
Monitoring specific values
If your service must respond immediately when a configuration value changes—rather than waiting until the next request—the ConfigReader offers a watch API powered by Swift’s async/await.
The example below shows a long-running task that follows the Service protocol from the Swift Service Lifecycle library. It leverages the ConfigReader watch API to log updates whenever the file is modified.
struct ConfigWatchReporter: Service {
let config: ConfigReader
let logger: Logger
func run() async throws {
try await self.config.scoped(to: "app")
.watchString(forKey: "name", default: "unset") { updates in
for try await update in updates.cancelOnGracefulShutdown() {
logger.info("Received a configuration change: (update)")
}
}
}
}
The watch API delivers updates as an async sequence of values. The cancelOnGracefulShutdown() method ensures the service exits cleanly. Add the reporter to your app alongside the ReloadingFileProvider during setup:
let configReporter = ConfigWatchReporter(config: config, logger: logger)
let app = Application(
router: router,
configuration: ApplicationConfiguration(reader: config.scoped(to: "http")),
services: [
reloadingProvider,
configReporter,
],
logger: logger
)
Consistent snapshots and avoiding torn reads
Hot reloading—whether handled by ReloadingFileProvider polling or the watch API—brings up an important edge case: if two reads within the same request return different configuration versions, you get a torn read. For instance, a middleware might read a rate limit value from config, and the handler that follows could see a different value because the file reloaded in between. Torn reads are unpredictable and notoriously hard to catch in tests.
Swift Configuration solves this with snapshots. A configuration snapshot is an immutable record of the entire configuration state at a specific moment. This is a protocol-level requirement—every provider must guarantee it, not just ReloadingFileProvider. When a reload happens, the provider swaps the snapshot reference in one atomic operation—replacing the whole key-value map at once, not key by key. This ensures no reader ever sees a half-updated state.
Snapshots also protect against bad reloads. If a reload produces invalid or unparseable data, ReloadingFileProvider keeps the last working snapshot instead of overwriting it. Your service keeps running on the previous valid configuration, and the failed reload gets logged so you can investigate without any downtime.
If your logic involves multiple reads and you need a guaranteed consistent view across all of them, grab a snapshot directly from the provider API.
Bringing it all together: the Hummingbird integration
With each component covered, here’s how they come together to build a working Kubernetes service using Hummingbird.
A complete working example from this post—including the Kubernetes manifests—is available in the reloading-example directory of the repository.
The app is first assembled using a static configuration. This static setup bootstraps a reloading provider, and then all providers are merged to create the configuration reader for the Hummingbird application.
func buildApplication(initialConfigProviders: [(any ConfigProvider)]) async throws
-> some ApplicationProtocol
{
// Create an initial configuration reader to bootstrap readers
// that depend on it, such as a ReloadingFileProvider, and
// setting up logging.
let initConfig = ConfigReader(providers: initialConfigProviders)
let logger = {
var logger = Logger(label: initConfig.string(
forKey: "http.serverName",
default: "default-HB-server"
))
logger.logLevel = initConfig.string(
forKey: "log.level",
as: Logger.Level.self,
default: .info)
return logger
}()
// Create a dynamic configuration provider that watches a file
// for changes. When the file changes, it reloads. The file path and
// polling interval are read from the initial configuration reader.
let reloadingProvider = try await ReloadingFileProvider(
config: initConfig.scoped(to: "config")
)
// Assemble a final configuration reader that includes
// the dynamic provider
let config = ConfigReader(
providers: [reloadingProvider] + initialConfigProviders,
accessReporter: AccessLogger(logger: logger)
)
let configReporter = ConfigWatchReporter(
config: config,
logger: logger)
// Assemble the routes for the app.
let router = try buildRouter(config: config)
// Create the hummingbird app and add the services to it.
// This runs a background service that watches for filesystem
// changes for configuration, and another that reports changes
// to a specific configuration value.
let app = Application(
router: router,
configuration: ApplicationConfiguration(reader: config.scoped(to: "http")),
services: [
reloadingProvider,
configReporter,
],
logger: logger
)
return app
}
The two-phase approach—starting with a bootstrap ConfigReader and then building the full reader—exists because ReloadingFileProvider needs the file path and poll interval before it can begin. Pulling those values from the initial static providers and then layering the dynamic provider on top prevents a circular dependency.
The full ConfigReader uses an AccessLogger connected to the application logger. The AccessLogger tracks every configuration value access and makes sure secrets aren’t written to logs in plain text.
Getting started
To add Swift Configuration to your own package, make sure to select the traits for the features you want when adding the dependency to your project’s Package.swift.
The example below shows how to enable reloading, YAML, and command line arguments alongside the default providers:
.package(
url: "
from: "1.0.0",
traits: [.defaults, "CommandLineArguments", "Reloading", "YAML"]
)
Get involved
Documentation for Swift Configuration is on the Swift Package Index, a video walkthrough is available on YouTube, and a post on the Swift Blog introduces its capabilities.
Swift Configuration has reached 1.0 and is ready for production. Open an issue if you run into any problems, and check the CONTRIBUTING guide to submit a pull request. The provider protocol is open—if your stack requires a format or source that doesn’t exist yet, you can build it. We’d love to hear how this works for your use case, and if there’s anything missing that you need for production services written in Swift.



