> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# TimescaleDB Highly Available

> Deploy a highly available TimescaleDB cluster on Control Plane. A Patroni-managed PostgreSQL 18 + TimescaleDB time-series cluster with etcd consensus, HAProxy leader routing, automatic failover, and optional PgBouncer pooling and scheduled backups.

<Warning>
  **Two things to check when upgrading an existing cluster.**

  **Confirm a recent backup actually completed.** This template takes scheduled *logical* backups as a cron workload. A dump that is killed part-way through logs nothing, so a schedule that looks healthy is not on its own evidence of a usable backup. Check the recent runs, and confirm the object landed in your bucket:

  ```bash theme={null}
  cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-timescaledb-ha-backup"}' --since 48h
  ```

  **Upgrading does not retune a running cluster.** Patroni's consensus timeouts are written to etcd when the cluster is first created, and the chart is never read again afterwards. A new install picks up the current values; an existing cluster keeps the ones it was created with. To move a running cluster, use the `patronictl edit-config` command in the template README.
</Warning>

## Overview

TimescaleDB Highly Available deploys the TimescaleDB time-series database (a PostgreSQL 18 extension) as a Patroni-managed cluster with automatic failover. Three replicas — one leader plus two hot standbys — form the cluster over an etcd consensus store, and an HAProxy leader endpoint routes all writes to the current primary. When the leader fails, Patroni promotes a standby and HAProxy re-selects the new primary automatically, giving applications a single stable endpoint. Optional PgBouncer connection pooling and scheduled logical backups to AWS S3, GCS, or MinIO are included.

Database credentials are **not** template values. The cluster reads its username, password and database name from a dictionary secret you create before installing, so no password passes through Helm or lands in the release.

<Warning>
  **Template version 1.1.0 is a breaking security change.** `postgres.username`, `postgres.password` and `postgres.database` were removed, along with `backup.minio.accessKey` and `backup.minio.secretKey`. An install or upgrade that still sets any of them now fails at render instead of silently falling back to a published default password. If you are running 1.0.1 or earlier, read [Upgrading From 1.0.x](#upgrading-from-1-0-x) before you touch the release.
</Warning>

<Info>
  TimescaleDB is licensed under the Timescale License (TSL) plus the PostgreSQL License. It is free to self-host at any scale, including all Community features (compression, continuous aggregates, retention); the license only forbids reselling TimescaleDB itself as a managed database service.
</Info>

<Note>
  For high availability, keep at least 3 TimescaleDB replicas and 3 etcd replicas. etcd requires an odd number of replicas (3, 5, 7) for quorum. `replicas: 1` renders a single-member cluster with no failover.
</Note>

### What Gets Created

* **Stateful TimescaleDB Workload** — A Patroni-managed PostgreSQL 18 + TimescaleDB 2.28.3 cluster on port `5432` (`replicas: 3` by default: 1 leader + 2 hot standbys). Each replica has per-replica DNS and its own volume; the extension is preloaded and created automatically in your database. Leadership is handed off before any replica is shut down, keeping writes available during deployments and restarts.
* **etcd Workload** — A dedicated etcd cluster providing the distributed consensus store (DCS) for Patroni leader election, from the `etcd` subchart dependency.
* **HAProxy Leader-Routing Workload** *(enabled by default)* — Routes write traffic to the current primary via Patroni's REST health check, providing a stable connection endpoint across failover.
* **PgBouncer Workload** *(optional)* — A connection pooler deployed in front of HAProxy, multiplexing application connections into a smaller pool of real database connections.
* **Backup Cron Workload** *(optional)* — A scheduled `pg_dumpall` logical backup that dumps the leader through HAProxy and writes a compressed SQL dump to AWS S3, GCS, or MinIO.
* **Volume Sets** — Per-replica persistent storage for the TimescaleDB and etcd data directories, with optional autoscaling.
* **Secrets** — A dictionary secret with database credentials, plus opaque secrets for the Patroni and HAProxy start scripts.
* **Identity & Policy** — An identity bound to the workload with least-privilege `reveal` access to exactly those secrets, and scoped cloud storage access when backup is enabled.

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC.
</Note>

## Prerequisites

**One `dictionary` secret must exist before you install.** These are the credentials you type into every connection string, so they are not values — a value would leave them in the Helm release.

```bash theme={null}
cpln secret create-dictionary --name my-timescaledb-ha-credentials \
  --entry username=myuser \
  --entry password='YOUR-STRONG-PASSWORD' \
  --entry database=mydb
```

Set `postgres.credentialsSecretName` to the name you used. The TimescaleDB extension is created automatically in the database that secret names. Secret names are org-wide, so give each release its own.

Backing up to MinIO needs a second dictionary secret holding `accessKey` and `secretKey` — see [Backup Prerequisites](#backup-prerequisites).

<Warning>
  **Create the secret before installing, or the deployment wedges silently.** The template refuses to render when `postgres.credentialsSecretName` is blank, but a name pointing at a secret that does not exist installs "successfully" and then never starts. The container never runs, so `cpln logs` returns **zero lines** — there is nothing to log. The one place the reason appears is `status.versions[].message`:

  ```bash theme={null}
  cpln workload get-deployments RELEASE_NAME-timescaledb-ha --gvc GVC_NAME -o yaml
  ```

  Use `get-deployments` — plain `cpln workload get` has no `versions` key. Creating the missing secret repairs it on its own in roughly **5.5 to 10.5 minutes**, or run `cpln workload force-redeployment RELEASE_NAME-timescaledb-ha --gvc GVC_NAME` to clear it in about 90 seconds.
</Warning>

## Upgrading From 1.0.x

Version 1.1.0 removes `postgres.username`, `postgres.password` and `postgres.database`, plus `backup.minio.accessKey` and `backup.minio.secretKey`.

<Warning>
  **Carrying the old values forward stops the upgrade.** The removed keys are rejected at render, so `cpln helm upgrade` fails and your existing cluster is left untouched and running:

  ```text theme={null}
  Error: execution error at (timescaledb-highly-available/templates/identity.yaml:2:4):
  timescaledb-highly-available: postgres.username, postgres.password and postgres.database were
  REMOVED — they are now a `dictionary` secret you create, named by postgres.credentialsSecretName,
  holding the keys `username`, `password` and `database`. Delete them from your values.
  ```
</Warning>

<Steps>
  <Step title="Create the secrets with the credentials the cluster already has">
    Credentials were baked into the data directory at bootstrap, so a new value does not change the cluster. Use your existing values.
  </Step>

  <Step title="Remove the old keys and upgrade">
    Delete the five removed keys and set the two `credentialsSecretName` values.
  </Step>

  <Step title="Rotate a default password">
    `password` was published in the public template repository. Rotate with `ALTER ROLE` through the HAProxy leader endpoint, then update the secret to match.
  </Step>
</Steps>

<Note>
  **Patroni now reads the credentials at container start**, rather than having them rendered into its configuration. The Patroni config is generated by the startup script from environment variables the platform resolves from your secret — a `cpln://` reference written into a config file would never be resolved. No configuration knobs changed.
</Note>

## Installation

This template has no external prerequisites unless backup is enabled. To install, follow the instructions for your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
replicas: 3 # 1 leader + 2 hot standbys; Patroni forms the cluster and handles failover

resources:
  minCpu: 500m
  minMemory: 1Gi # time-series ingest is memory-sensitive
  maxCpu: 1
  maxMemory: 2Gi

image: timescale/timescaledb-ha:pg18.4-ts2.28.3 # PostgreSQL 18 + TimescaleDB 2.28.3 Community, Patroni bundled

postgres:
  credentialsSecretName: my-timescaledb-ha-credentials # see Prerequisites — must exist before install

multiZone: false # spread replicas across zones in the location

volumeset:
  capacity: 10 # initial capacity in GiB per replica (minimum 10)
  autoscaling:
    enabled: false # set to true to grow each volume automatically
    maxCapacity: 100 # maximum capacity in GiB when autoscaling is enabled
    minFreePercentage: 10 # free-space threshold that triggers scaling
    scalingFactor: 1.2 # how much to scale up by

internal_access:
  type: same-gvc # options: same-gvc, same-org, workload-list
  workloads: # only when type is same-gvc or workload-list
    #- //gvc/GVC_NAME/workload/WORKLOAD_NAME

etcd:
  replicas: 3 # use an odd number (3, 5, 7) for quorum
  resources:
    cpu: 500m
    memory: 512Mi
  multiZone: false
  tuning:
    # Passed through to the bundled etcd. Without compaction its backend grows
    # with time alone — Patroni renews its lease every ~10s and every renewal is
    # a revision — until it hits the quota and etcd goes READ-ONLY.
    autoCompactionMode: periodic # periodic (retention is a duration) or revision (a revision count)
    autoCompactionRetention: 1h # periodic needs an explicit unit (1h, 30m, 24h); revision takes a count
    quotaBackendBytes: 0 # backend size limit in bytes; 0 = etcd's own default of 2 GiB
  volumeset:
    capacity: 10
  internal_access:
    type: same-gvc

pgbouncer:
  enabled: false
  image: edoburu/pgbouncer:v1.25.1-p0
  poolMode: transaction # options: session, transaction, statement
  defaultPoolSize: 25 # real Postgres connections PgBouncer maintains per pod
  maxClientConn: 1000 # maximum client connections PgBouncer accepts per pod
  maxDbConnections: 100 # hard cap on total Postgres connections across all pods
  minReplicas: 2
  maxReplicas: 4
  resources:
    cpu: 200m
    memory: 128Mi

proxy: # HAProxy leader endpoint; auto-enabled when pgbouncer is enabled, required for logical backups
  enabled: true
  image: haproxy:2.9
  resources:
    cpu: 100m
    memory: 128Mi
  minReplicas: 2
  maxReplicas: 2

backup:
  enabled: false
  image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0 # PG18 client, matches server major
  schedule: "0 2 * * *" # daily at 2am UTC
  resources:
    cpu: 100m
    memory: 128Mi

  provider: aws # options: aws, gcp, minio

  aws:
    bucket: my-backup-bucket
    region: us-east-1
    cloudAccountName: my-backup-cloudaccount
    policyName: my-backup-policy
    prefix: timescaledb/backups # folder where backups are stored

  gcp:
    bucket: my-backup-bucket
    cloudAccountName: my-backup-cloudaccount
    prefix: timescaledb/backups

  minio: # self-hosted MinIO or any S3-compatible endpoint
    endpoint: http://my-minio-workload:9000 # e.g. http://WORKLOAD_NAME:9000 for an internal MinIO deployment
    bucket: my-backup-bucket
    credentialsSecretName: my-timescaledb-ha-minio-credentials # dictionary secret with accessKey and secretKey
    prefix: timescaledb/backups
```

### Credentials

* `postgres.username` — Database superuser username. **Change before deploying to production.**
* `postgres.password` — Database superuser password. **Change before deploying to production.**
* `postgres.database` — Name of the database created on first startup. The TimescaleDB extension is created automatically inside it.

<Note>
  These values are only applied on first startup when the data directory is empty. Updating them after the initial deployment has no effect on the running database. To change credentials or the database name on an existing cluster, use PostgreSQL's native commands (e.g. `ALTER USER`, `ALTER DATABASE`).
</Note>

### Cluster and Resources

* `replicas` — Number of TimescaleDB replicas (default `3`: 1 leader + 2 hot standbys). Use at least 3 for high availability.
* `image` — The TimescaleDB HA image tag (PostgreSQL 18 + TimescaleDB 2.28.3 Community, with Patroni bundled).
* `resources.minCpu` / `resources.minMemory` — Minimum CPU and memory guaranteed per replica.
* `resources.maxCpu` / `resources.maxMemory` — Maximum CPU and memory per replica.
* `multiZone` — Spread replicas across availability zones within the location.

### Storage

* `volumeset.capacity` — Initial volume size in GiB (minimum 10). Each replica gets its own volume.
* `volumeset.autoscaling.enabled` — Automatically expand the volume as it fills. When enabled:
  * `maxCapacity` — Maximum volume size in GiB.
  * `minFreePercentage` — Trigger a scale-up when free space drops below this percentage.
  * `scalingFactor` — Multiply the current capacity by this factor when scaling up.

### etcd Cluster

* `etcd.replicas` — Number of etcd replicas. Must be an **odd number** (3, 5, 7) for quorum.
* `etcd.resources.cpu` / `etcd.resources.memory` — CPU and memory per etcd replica.
* `etcd.multiZone` — Spread etcd replicas across availability zones.
* `etcd.volumeset.capacity` — Initial volume size for etcd data in GiB.
* `etcd.internal_access.type` — Controls which workloads can reach the etcd cluster.

<Note>
  etcd is a hard dependency. If the etcd members are unhealthy, Patroni loses its consensus store and the cluster goes read-only. If writes start failing, check the `RELEASE_NAME-etcd` workload first.
</Note>

#### etcd History Compaction

`etcd.tuning.autoCompactionMode`, `etcd.tuning.autoCompactionRetention` and `etcd.tuning.quotaBackendBytes` control how much revision history the bundled etcd cluster keeps and how large its backend may grow. The defaults — `periodic`, `1h` and `0` (etcd's own 2 GiB limit) — are the right settings for a Patroni consensus store and should be left alone. The [etcd template](/template-catalog/templates/etcd#compaction-and-backend-growth) documents the mechanism, the accepted value formats, and the read-only commands for inspecting a cluster.

<Warning>
  **Template version `1.0.0` did not compact the bundled etcd cluster.** Patroni renews its leader lease about every 10 seconds and every renewal creates a revision, so etcd grows with time alone — roughly 19 MB per day on an idle cluster — and reaches its 2 GiB backend quota in about 110 days. etcd then goes read-only across the whole cluster, and the TimescaleDB replicas restart-loop with `exitCode: 0` and `reason: Completed`, which reads as healthy and is easily mistaken for a database fault. Upgrade to `1.0.1` or later to turn compaction on.

  Upgrading prevents further growth but cannot shrink a backend that has already grown, and a cluster that has already raised a `NOSPACE` alarm needs operator recovery rather than an upgrade — see [If the Backend Quota Is Already Full](/template-catalog/templates/etcd#if-the-backend-quota-is-already-full).
</Warning>

### HAProxy Leader Endpoint

In a Patroni cluster, only the leader replica accepts writes — the standbys are read-only. HAProxy automatically routes writes to the current leader by health-checking each replica's Patroni REST endpoint, giving applications a stable connection endpoint even during failover. It is enabled by default.

* `proxy.enabled` — Deploy the HAProxy leader-routing workload (default: `true`).
* `proxy.resources.cpu` / `proxy.resources.memory` — CPU and memory per HAProxy replica.
* `proxy.minReplicas` / `proxy.maxReplicas` — Replica count for the proxy workload.

<Note>
  `proxy.enabled` must stay `true` for logical backups — the backup job dumps the leader through the proxy endpoint.
</Note>

### Internal Access

The cluster is internal-only; there is no public endpoint. Reach it from other workloads through the proxy (or PgBouncer).

* `internal_access.type` — Controls which workloads can connect to the cluster on port `5432`:

| Type            | Description                                                     |
| --------------- | --------------------------------------------------------------- |
| `same-gvc`      | Allow access from all workloads in the same GVC                 |
| `same-org`      | Allow access from all workloads in the same organization        |
| `workload-list` | Allow access only from specific workloads listed in `workloads` |

* `internal_access.workloads` — When `type` is `workload-list`, the list of workload links (e.g. `//gvc/GVC_NAME/workload/WORKLOAD_NAME`) allowed to connect.

### PgBouncer Connection Pooling

PgBouncer is an optional connection pooler that sits in front of HAProxy, multiplexing application connections into a smaller pool of real database connections. HAProxy handles leader routing and failover transparently — PgBouncer is unaware of which replica is the primary.

Enabling PgBouncer **automatically enables HAProxy**, as it is required for leader-aware routing. When enabled, PgBouncer becomes the primary connection endpoint:

```text theme={null}
RELEASE_NAME-pgbouncer.GVC_NAME.cpln.local:5432
```

* `pgbouncer.poolMode` — Controls how connections are reused:

| Mode          | Description                                                                                                                                                                                                             |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transaction` | Connection held only for the duration of a transaction, then returned to the pool. Best for most web and API workloads. Not compatible with session-level features (`SET` variables, temporary tables, advisory locks). |
| `session`     | Connection held for the entire client session. Compatible with all PostgreSQL features but provides less reuse.                                                                                                         |
| `statement`   | Connection returned after every statement. Transactions are not supported. Rarely used.                                                                                                                                 |

* `pgbouncer.defaultPoolSize` — Number of real database connections PgBouncer maintains per pod (default: `25`).
* `pgbouncer.maxClientConn` — Maximum number of client connections PgBouncer accepts per pod (default: `1000`).
* `pgbouncer.maxDbConnections` — Hard cap on total PostgreSQL connections across **all** PgBouncer pods (default: `100`). Prevents connection blowout when PgBouncer scales out.
* `pgbouncer.minReplicas` / `pgbouncer.maxReplicas` — PgBouncer autoscales between these bounds.
* `pgbouncer.resources.cpu` / `pgbouncer.resources.memory` — Resources allocated to each PgBouncer pod.

### Backup

Backup is disabled by default. When enabled, a cron workload runs `pg_dumpall` against the leader (through HAProxy) on the configured schedule and uploads a compressed SQL dump to AWS S3, GCS, or a MinIO-compatible endpoint.

* `backup.enabled` — Enable scheduled logical backups.
* `backup.image` — The backup container image. Match its tag to the server major version — `18.1.0` for the default PostgreSQL 18 image.
* `backup.schedule` — Cron expression for backup frequency (default: daily at 2am UTC).
* `backup.provider` — `aws`, `gcp`, or `minio`.
* `backup.resources.cpu` / `backup.resources.memory` — Resources for the backup cron container.

Complete the [backup prerequisites](#backup-prerequisites) for your provider before enabling backup.

## Connecting

Always connect through the proxy (or PgBouncer) — never a raw replica address, since the leader moves on failover.

| What                           | Value                                                                                     |
| ------------------------------ | ----------------------------------------------------------------------------------------- |
| Via HAProxy (default)          | `RELEASE_NAME-timescaledb-ha-proxy.GVC_NAME.cpln.local:5432`                              |
| Via PgBouncer *(when enabled)* | `RELEASE_NAME-pgbouncer.GVC_NAME.cpln.local:5432` — use this as your application endpoint |
| Credentials                    | `postgres.username` / `postgres.password`                                                 |
| Database                       | `postgres.database` (TimescaleDB extension created here)                                  |

## Using TimescaleDB

Any PostgreSQL client or ORM works unchanged. Turn a regular table into a hypertable (automatically partitioned by time) and query it with time buckets:

```sql theme={null}
CREATE TABLE metrics (time timestamptz NOT NULL, device text, value double precision);
SELECT create_hypertable('metrics', by_range('time'));

INSERT INTO metrics VALUES (now(), 'sensor-1', 23.5);

SELECT time_bucket('1 hour', time) AS bucket, device, avg(value)
FROM metrics
GROUP BY bucket, device
ORDER BY bucket;
```

From here you can add columnar compression, continuous aggregates, and retention policies — all Community features included in the default image.

### Availability During Failover and Upgrades

The cluster keeps writes available across leader changes, but applications should implement connection retry with exponential backoff — a few seconds of write interruption is expected at each leader restart:

* **Automatic failover** — When the leader fails, Patroni promotes a standby (a manual/orchestrated promotion completes in roughly 5 seconds in testing, with no committed rows lost) and HAProxy re-selects the new primary automatically, with no client change.
* **Rolling restarts and upgrades** — Each replica is restarted in turn; leadership is handed off before the leader shuts down. Expect a brief write gap (a few seconds) at each leader switchover during the roll.

## Backup Prerequisites

### AWS S3

Before enabling backup with `provider: aws`, complete the following in your AWS account:

<Steps>
  <Step title="Create a bucket">
    Create an S3 bucket. Set `backup.aws.bucket` to its name and `backup.aws.region` to its region.
  </Step>

  <Step title="Set up a Cloud Account">
    If you do not have one, [create a Cloud Account](/guides/create-cloud-account) for your AWS account. Set `backup.aws.cloudAccountName` to its name.
  </Step>

  <Step title="Create an IAM policy">
    Create an IAM policy with the JSON below, replacing `YOUR_BUCKET_NAME`, then set `backup.aws.policyName` to the policy's name and `backup.aws.prefix` to the folder path for backups.
  </Step>
</Steps>

<Warning>
  **Version 1.1.1 narrows AWS backup permissions.** This version removes `aws::ReadOnlyAccess` from the backup identity. That AWS managed policy granted read access to **every bucket in your AWS account** and contains no write actions at all, so it was never carrying the backup itself — but it *was* silently supplying any read action your own bucket-scoped policy happened to omit.

  **Update your IAM policy to the full action list below before upgrading.** If it already matches, no action is needed. The identity now carries `cpln-connector` and your bucket-scoped policy only, which is strictly narrower than before. Nothing else changes.
</Warning>

```json theme={null}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetObjectVersion",
                "s3:DeleteObjectVersion",
                "s3:GetBucketLocation",
                "s3:AbortMultipartUpload",
                "s3:ListBucketMultipartUploads",
                "s3:ListMultipartUploadParts"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME",
                "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            ]
        }
    ]
}
```

### GCS

Before enabling backup with `provider: gcp`, complete the following in your GCP account:

<Steps>
  <Step title="Create a bucket">
    Create a GCS bucket. Set `backup.gcp.bucket` to its name.
  </Step>

  <Step title="Set up a Cloud Account">
    If you do not have one, [create a Cloud Account](/guides/create-cloud-account) for your GCP project. Set `backup.gcp.cloudAccountName` to its name — access is keyless, with no stored credentials.
  </Step>
</Steps>

<Warning>
  Grant the **Storage Admin** role (`roles/storage.objectAdmin` scoped to the bucket also works) to the GCP service account created for the Cloud Account. Set `backup.gcp.prefix` to the folder path for backups.
</Warning>

### MinIO

Before enabling backup with `provider: minio`, ensure your MinIO instance (or any S3-compatible endpoint) is accessible:

<Steps>
  <Step title="Create a bucket">
    Create a bucket on the server. Set `backup.minio.bucket` to its name.
  </Step>

  <Step title="Set the endpoint">
    Set `backup.minio.endpoint` to the S3 API address including the port. For the `minio` marketplace template deployed in the same GVC, use `http://WORKLOAD_NAME:9000`.
  </Step>

  <Step title="Set credentials">
    Create a second [dictionary secret](/guides/create-secret/dictionary) holding exactly `accessKey` and `secretKey`, and set `backup.minio.credentialsSecretName` to its name. For the MinIO template these are its `admin.username` and `admin.password`. Set `backup.minio.prefix` to the folder path for backups.
  </Step>
</Steps>

<Note>
  MinIO backup requires no Control Plane Cloud Account — credentials are passed directly to the backup job.
</Note>

## Restoring a Backup

Restoring a TimescaleDB dump is **not** the vanilla PostgreSQL procedure. The target server must run the **same TimescaleDB extension version** as the dump, and the replay must be wrapped in `timescaledb_pre_restore()` and `timescaledb_post_restore()`. Connect through the proxy so the restore targets the current leader.

<Warning>
  Restore replays a full-cluster dump. Run it against a fresh cluster, not a database that already holds data you want to keep.
</Warning>

**AWS S3:**

```sh theme={null}
export PGPASSWORD="PASSWORD"

psql -h RELEASE_NAME-timescaledb-ha-proxy.GVC_NAME.cpln.local -U USERNAME -d DATABASE \
  -c "SELECT timescaledb_pre_restore();"

aws s3 cp "s3://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" - \
  | gunzip \
  | psql -h RELEASE_NAME-timescaledb-ha-proxy.GVC_NAME.cpln.local -p 5432 -U USERNAME -d postgres

psql -h RELEASE_NAME-timescaledb-ha-proxy.GVC_NAME.cpln.local -U USERNAME -d DATABASE \
  -c "SELECT timescaledb_post_restore();"

unset PGPASSWORD
```

For **GCS**, replace the download with `gsutil cp "gs://BUCKET_NAME/PREFIX/BACKUP_FILE.sql.gz" -`. For **MinIO**, add `--endpoint-url "http://MINIO_ENDPOINT:9000"` to the `aws s3 cp` command and export the MinIO access and secret keys as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.

## Important Notes

* **Change `postgres.password` before installing.** Credentials are baked into the data directory at first boot; changing the value later does not change the database password. To reset, uninstall (which deletes the volumes) and reinstall.
* **Always connect via the proxy, never a replica.** The leader moves on failover; a pinned replica address will break. PgBouncer and backups already route through the proxy.
* **`replicas: 1` has no HA.** It renders a single-member Patroni cluster with no failover. Use at least 3, and an odd `etcd.replicas` (3, 5, 7) for quorum.
* **etcd is a hard dependency.** If the etcd members are unhealthy, Patroni loses its consensus store and the cluster goes read-only — check `RELEASE_NAME-etcd` first if writes fail.
* **`proxy.enabled` must stay `true` for backups.** The logical backup dumps the leader through the proxy endpoint.
* **Backups are logical-only.** A scheduled `pg_dumpall`; continuous WAL archiving / point-in-time restore is a planned follow-up.
* **Expect a brief write interruption per leader restart.** During failover or a rolling upgrade, applications should retry with exponential backoff.

## External References

<CardGroup cols={2}>
  <Card title="TimescaleDB Documentation" icon="database" href="https://docs.timescale.com/">
    Official TimescaleDB documentation
  </Card>

  <Card title="Hypertables" icon="table" href="https://docs.timescale.com/use-timescale/latest/hypertables/">
    Hypertables, compression, continuous aggregates, and retention
  </Card>

  <Card title="Patroni Documentation" icon="book" href="https://patroni.readthedocs.io/">
    Patroni automatic failover documentation
  </Card>

  <Card title="etcd Documentation" icon="server" href="https://etcd.io/docs/v3.6/">
    Official etcd documentation
  </Card>

  <Card title="Restoring a TimescaleDB Backup" icon="rotate-left" href="https://www.tigerdata.com/docs/reference/timescaledb/administration/timescaledb_pre_restore">
    Using timescaledb\_pre\_restore and timescaledb\_post\_restore
  </Card>

  <Card title="TimescaleDB HA Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/timescaledb-highly-available">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
