July 7, 2025·6 min read·engineeringrqlitebackupsgcsrecovery

rqlite 8.39.0 adds Google Cloud Storage to automatic backup and restore

The GCS adapter joins rqlite's existing backup pipeline. Its authentication, object metadata and buffering behavior define the practical deployment contract.

An automatic backup facility is easier to deploy when it can use the object store already available to the application. rqlite 8.39.0, published on 7 July, adds Google Cloud Storage to automatic backup and restore. The implementation plugs a GCS client into the existing uploader and downloader. It does not introduce a separate database recovery model for Google Cloud.

I value rqlite for keeping distributed relational state operationally proportionate. Supporting another storage service inside the existing backup pipeline fits that preference. The interesting details are how little the generic pipeline needs to know about GCS, and which provider-specific behaviors remain visible to the operator.

Storage selection moves into configuration construction

PR #2145 changes backup configuration handling to return a storage client alongside the parsed configuration. A switch on the storage type constructs either the existing S3 client or the new GCS client. Unsupported types return an explicit error.

Restore configuration follows the same pattern. The generic downloader receives a client implementing its download interface. It does not need the bucket name, credentials format, or HTTP endpoints as separate arguments. Those choices remain inside the provider adapter.

This is a useful division because backup scheduling and remote object access change for different reasons. A new provider needs configuration decoding and an implementation of the storage operations. It does not need another copy of the upload loop or gzip detection.

An example GCS backup configuration uses the new storage type and provider-specific subconfiguration. The paths and names below are placeholders for a deployment that has already provisioned its bucket and credential file.

{
  "version": 1,
  "type": "gcs",
  "interval": "24h",
  "sub": {
    "bucket": "example-backups",
    "name": "database.sqlite",
    "project_id": "example-project",
    "credentials_path": "/run/credentials/gcs.json"
  }
}

The field names come from the configuration test added with the integration and the released GCS configuration structure. The example selects one object name. It does not describe a directory of automatically retained generations.

Credentials are a file input in this implementation

The GCS client constructor reads the file named by credentials_path. Its service-account structure decodes a client email and private key. Token creation signs a JWT using the account's RSA private key and exchanges that assertion at Google's OAuth token endpoint.

The source selects the storage read-write scope and an RS256 signature. A successful token exchange supplies an access token and lifetime. The client caches that token behind a mutex and reuses it while more than two minutes remain before its recorded expiry. Storage requests attach that token as bearer authentication.

Those are implementation facts with deployment consequences. The credential path must name a file readable by the database process. The constructor shown here does not implement a general search through ambient credentials or a metadata-server identity flow. An operator should not infer those capabilities merely because other Google Cloud software supports them.

The token cache also differs from reading the service-account file afresh for every upload. Construction loads the account into the client. Subsequent token renewal uses that stored account information. Replacing the file on disk is therefore not the same operation as refreshing an expired access token inside this client.

Authentication and storage access remain separate failure stages. Reading the credential file can fail during client construction. Signing or exchanging a token can fail later when a request needs authentication. An object upload cannot be considered configured successfully merely because JSON decoding succeeded.

The upload includes a database-state identifier

The generic uploader asks its data provider for the cluster-wide index at which the data last changed. It compares that value against the last index uploaded by this uploader instance. If the data has not advanced, it increments a skipped-upload statistic and returns.

When an upload is needed, the provider writes the backup into a temporary file. The uploader then seeks to the start and passes a reader plus the index's decimal representation to the storage client. The adapter receives the data and an identifier, without needing to understand SQL.

The GCS adapter stores that identifier in object metadata under id. Its multipart request contains a JSON metadata part and a binary data part. Keeping those together lets the object carry the marker that the generic uploader later uses to recognize already backed-up state.

On the first upload attempt after the uploader starts, it asks the storage client for the current remote identifier. If that identifier matches the provider's index, it can skip transferring the backup. This check goes to storage rather than relying on an in-process cache that vanished during restart.

The marker is an optimization for deciding whether another transfer is needed. It is not a separate archive catalog or a row-level change log. The adapter stores the supplied identifier beside the configured object, and the uploader compares it against its data provider's position.

The first adapter buffers the multipart request

PR #2144 introduces the GCS adapter and its HTTP operations. Its upload method builds the multipart body in a bytes.Buffer. It copies the backup reader into that body before issuing the HTTP request.

This is relevant to rqlite's otherwise compact deployment. The generic pipeline already creates a temporary file for the backup. During provider upload, the multipart representation occupies memory. A reader interface does not imply that this adapter streams the entire transfer with constant memory usage.

The code establishes that allocation pattern without establishing a measured memory cost for any particular database. Backup size and optional transformations determine the amount of content supplied to the adapter. Operators sizing a node need to account for the implementation they are running, rather than assuming every storage client has identical buffering behavior.

Download takes a different path. The adapter reads the HTTP response into a fixed-size buffer and writes successive portions through an io.WriterAt. The released default buffer is 32 KiB. That describes the provider's transfer loop, while the surrounding restore pipeline has its own temporary-file handling.

Restore reuses gzip detection and timeout handling

The restore configuration selects GCS through the same type switch used for S3. If no timeout is supplied, it assigns thirty seconds. The configuration also carries continue_on_failure, which controls how the download wrapper reports an allowed download failure to its caller.

The downloader creates a temporary file and applies a timeout context to storage download. It then examines the initial bytes for the gzip signature. Compressed data goes through a gzip reader, while uncompressed data copies directly to the destination writer.

That reuse is the main architectural benefit of the integration. GCS does not need a provider-specific compression format, and the downloader does not need to understand Google's authentication. Each layer preserves the behavior it already owns.

The failure option has a narrower role than its name might suggest. Configuration-reading and client-construction failures return before the download call that consults continue_on_failure. The wrapper specifically marks a failed download according to that setting. It is not a blanket instruction to ignore every error anywhere in initialization.

My preference is to keep those stages explicit. Recovering data from object storage needs a well-defined source and an understandable failure result. Sharing the downloader preserves those decisions while adding a new way to obtain the bytes.

A configured object remains a configured object

The GCS adapter builds its object URL from the configured bucket and name. Upload metadata uses that same name. Although the generic backup configuration has a timestamp option used by the S3 client, the GCS construction shown in this release does not pass an equivalent timestamp behavior into its adapter.

That is a useful limit when evaluating examples. Recognizing a field in the generic configuration does not prove that every storage provider implements identical naming behavior. The released GCS code supplies a particular object name to each upload.

The changelog dates the change to 6 July, while GitHub publishes the release on 7 July. This post uses that publication date. The delivered capability is automatic backup and restore through GCS using the shared pipeline, with file-based service-account credentials and a provider adapter whose object naming and transfer behavior are visible in the versioned source.