Skip to content

HeliosProxy Integration Guide

How to integrate HeliosProxy into your application stack.


Quick Start

1. Install HeliosProxy

HeliosProxy is a separate, Apache-2.0 product. Install it from crates.io or use the container image:

Terminal window
# Default build (connection pooling modes)
cargo install heliosdb-proxy
# Every optional feature plus the PostgreSQL topology provider
cargo install heliosdb-proxy --features "all-features,postgres-topology"
# Container image (built with all-features,postgres-topology)
# Pin a release tag in production
docker pull ghcr.io/heliosdatabase/hdb-heliosdb-proxy:latest

2. Create Configuration

/etc/heliosproxy/config.toml
listen_address = "127.0.0.1:5433"
admin_address = "127.0.0.1:9090"
tr_enabled = true
tr_mode = "session"
[pool]
min_connections = 2
max_connections = 100
idle_timeout_secs = 300
max_lifetime_secs = 1800
acquire_timeout_secs = 30
test_on_acquire = true
[load_balancer]
read_strategy = "least_connections"
read_write_split = true
latency_threshold_ms = 100
[health]
check_interval_secs = 5
check_timeout_secs = 3
failure_threshold = 3
success_threshold = 2
check_query = "SELECT 1"
[[nodes]]
host = "127.0.0.1"
port = 5432
role = "primary"
weight = 100
enabled = true

[pool], [load_balancer], [health] and at least one [[nodes]] entry (with weight and enabled) are required. [pool_mode] is optional and defaults to session pooling. See the configuration reference.

3. Start the Proxy

Terminal window
heliosdb-proxy --config /etc/heliosproxy/config.toml

4. Connect Applications

Terminal window
# Connect via proxy instead of directly to database
psql -h localhost -p 5433 -U myuser -d mydb

Application Integration

Python (psycopg2/asyncpg)

# Before: Direct connection
conn = psycopg2.connect(
host="db.example.com",
port=5432,
database="mydb"
)
# After: Via HeliosProxy
conn = psycopg2.connect(
host="proxy.example.com",
port=5433, # Proxy port
database="mydb"
)

Node.js (pg)

// Before: Direct connection
const pool = new Pool({
host: 'db.example.com',
port: 5432,
database: 'mydb'
});
// After: Via HeliosProxy
const pool = new Pool({
host: 'proxy.example.com',
port: 5433, // Proxy port
database: 'mydb'
});

Java (JDBC)

// Before
String url = "jdbc:postgresql://db.example.com:5432/mydb";
// After
String url = "jdbc:postgresql://proxy.example.com:5433/mydb";

Go (pgx)

// Before
conn, err := pgx.Connect(ctx, "postgres://user:pass@db.example.com:5432/mydb")
// After
conn, err := pgx.Connect(ctx, "postgres://user:pass@proxy.example.com:5433/mydb")

Rust (tokio-postgres)

// Before
let (client, connection) = tokio_postgres::connect(
"host=db.example.com port=5432 dbname=mydb",
NoTls
).await?;
// After
let (client, connection) = tokio_postgres::connect(
"host=proxy.example.com port=5433 dbname=mydb",
NoTls
).await?;

Connection Pooling Best Practices

Choosing a Pool Mode

HeliosDB Lite backends: use session pooling (the default). Transaction and statement pooling reset each connection with DISCARD ALL when it returns to the pool, and DISCARD ALL is planned for Lite; until then the reset fails, HeliosProxy closes the backend connection instead of reusing it, and pooling gives no benefit.

Use CaseRecommended Mode with LiteReason
Web applicationssessionTransaction pooling needs DISCARD ALL, planned for Lite
MicroservicessessionTransaction pooling needs DISCARD ALL, planned for Lite
Background jobssessionLong-running operations
Prepared statementssessionStatements tied to connection
Simple SELECT queriessessionStatement pooling needs DISCARD ALL, planned for Lite

Transaction Mode Guidelines

These apply once DISCARD ALL is available in Lite; until then keep session pooling. When using mode = "transaction":

# Good: Transaction boundaries are clear
with conn.cursor() as cur:
cur.execute("BEGIN")
cur.execute("INSERT INTO orders VALUES (%s)", (order_id,))
cur.execute("COMMIT") # Connection returned to pool here
# Bad: Implicit transaction holds connection
with conn.cursor() as cur:
cur.execute("SELECT * FROM orders")
# Process results slowly...
# Connection held until next statement or disconnect

Statement Mode Guidelines

These apply once DISCARD ALL is available in Lite; until then keep session pooling. When using mode = "statement":

# Good: Simple, independent queries
results = []
for id in ids:
cur.execute("SELECT * FROM items WHERE id = %s", (id,))
results.append(cur.fetchone())
# Bad: Prepared statements won't work
cur.execute("PREPARE get_item AS SELECT * FROM items WHERE id = $1")
cur.execute("EXECUTE get_item(1)") # May fail - different connection

Query Routing with Hints

Use SQL comments to control routing:

-- Force to primary (for critical reads)
SELECT /* helios:route=primary */ balance FROM accounts WHERE id = 1;
-- Force to standby (for read replicas)
SELECT /* helios:route=standby */ * FROM analytics_data;
-- Skip cache (for real-time data)
SELECT /* helios:cache=skip */ * FROM live_metrics;
-- Multiple hints
SELECT /* helios:route=standby helios:cache=skip */ * FROM events;

Application Helper

def read_from_primary(query):
"""Execute query on primary node."""
return f"/* helios:route=primary */ {query}"
def read_from_replica(query):
"""Execute query on replica node."""
return f"/* helios:route=standby */ {query}"
# Usage
cur.execute(read_from_primary("SELECT balance FROM accounts WHERE id = %s"), (user_id,))

High Availability Setup

Multi-Node Configuration

listen_address = "0.0.0.0:5433"
admin_address = "127.0.0.1:9090" # keep the admin API on loopback, or set admin_token
tr_enabled = true
tr_mode = "session"
[pool_mode]
mode = "session" # transaction pooling needs DISCARD ALL, planned for Lite
max_pool_size = 50
[pool]
min_connections = 2
max_connections = 50
idle_timeout_secs = 300
max_lifetime_secs = 1800
acquire_timeout_secs = 30
test_on_acquire = true
[load_balancer]
read_strategy = "least_connections"
read_write_split = true
latency_threshold_ms = 100
[health]
check_interval_secs = 5
check_timeout_secs = 3
failure_threshold = 3
success_threshold = 2
check_query = "SELECT 1"
# Primary node
[[nodes]]
host = "db-primary.internal"
port = 5432
role = "primary"
weight = 100
enabled = true
# Standby for failover
[[nodes]]
host = "db-standby.internal"
port = 5432
role = "standby"
weight = 100
enabled = true
# Read replicas for scaling
[[nodes]]
host = "db-replica-1.internal"
port = 5432
role = "replica"
weight = 100
enabled = true
[[nodes]]
host = "db-replica-2.internal"
port = 5432
role = "replica"
weight = 100
enabled = true

Failover Behavior

  1. Primary fails: New writes wait up to write_timeout_secs for a healthy primary
  2. Standby promoted: An external HA manager promotes the standby; with the default static topology, update the node roles (reload with SIGHUP) or bring the primary back at the same address
  3. Replica fails: Traffic redistributed to remaining replicas
  4. All replicas fail: Reads fall back to primary

Kubernetes Deployment

ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
name: heliosproxy-config
data:
config.toml: |
listen_address = "0.0.0.0:5433"
# Non-loopback admin bind so kubelet probes can reach it: admin_token is then required.
# /healthz and /readyz stay open for probes; every other admin route needs the token.
admin_address = "0.0.0.0:9090"
admin_token = "${HELIOS_ADMIN_TOKEN}"
tr_enabled = true
tr_mode = "session"
[pool_mode]
mode = "session" # transaction pooling needs DISCARD ALL, planned for Lite
max_pool_size = 100
[pool]
min_connections = 2
max_connections = 100
idle_timeout_secs = 300
max_lifetime_secs = 1800
acquire_timeout_secs = 30
test_on_acquire = true
[load_balancer]
read_strategy = "round_robin"
read_write_split = true
latency_threshold_ms = 100
[health]
check_interval_secs = 5
check_timeout_secs = 3
failure_threshold = 3
success_threshold = 2
check_query = "SELECT 1"
[[nodes]]
host = "heliosdb-primary.default.svc.cluster.local"
port = 5432
role = "primary"
weight = 100
enabled = true
[[nodes]]
host = "heliosdb-standby.default.svc.cluster.local"
port = 5432
role = "standby"
weight = 100
enabled = true

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: heliosproxy
spec:
replicas: 2
selector:
matchLabels:
app: heliosproxy
template:
metadata:
labels:
app: heliosproxy
spec:
containers:
- name: heliosproxy
image: ghcr.io/heliosdatabase/hdb-heliosdb-proxy:latest # pin a release tag in production
args: ["--config", "/etc/heliosproxy/config.toml"]
env:
- name: HELIOS_ADMIN_TOKEN
valueFrom:
secretKeyRef:
name: heliosproxy-admin
key: token
ports:
- containerPort: 5433
name: postgres
- containerPort: 9090
name: admin
volumeMounts:
- name: config
mountPath: /etc/heliosproxy
livenessProbe:
httpGet:
path: /healthz
port: admin
initialDelaySeconds: 5
readinessProbe:
httpGet:
path: /readyz
port: admin
initialDelaySeconds: 2
volumes:
- name: config
configMap:
name: heliosproxy-config

Service

apiVersion: v1
kind: Service
metadata:
name: heliosproxy
spec:
selector:
app: heliosproxy
ports:
- name: postgres
port: 5433
targetPort: 5433

The Service exposes only the PostgreSQL port. Reach the admin API with kubectl port-forward rather than publishing it.


Docker Compose

HeliosDB Lite images are built from the licensed source distribution (no public image is published); see Docker Deployment. The HeliosProxy image is public on GHCR.

services:
heliosproxy:
image: ghcr.io/heliosdatabase/hdb-heliosdb-proxy:latest # pin a release tag in production
command: ["--config", "/etc/heliosproxy/config.toml"]
ports:
- "5433:5433"
volumes:
- ./config.toml:/etc/heliosproxy/config.toml:ro
depends_on:
- heliosdb
heliosdb:
image: heliosdb-lite:latest # built from the licensed source distribution
command: ["start", "--data-dir", "/data", "--listen", "0.0.0.0", "--port", "5432"]
environment:
- HELIOSDB_JWT_SECRET=${HELIOSDB_JWT_SECRET:?set HELIOSDB_JWT_SECRET}
volumes:
- heliosdb_data:/data
volumes:
heliosdb_data:

In config.toml, point the primary [[nodes]] entry at host = "heliosdb". For a primary with standbys, see the HA example in Docker Deployment.


Monitoring

The admin API serves server metrics as JSON at GET /metrics, and as Prometheus exposition text at GET /metrics/prometheus. That text arrives wrapped in a JSON text field, so a scraper must unwrap it. Counters include heliosdb_proxy_connections_total, heliosdb_proxy_queries_total, heliosdb_proxy_failovers_total and the Transaction Replay counters such as heliosdb_proxy_tr_failovers_total. The Admin API reference lists every counter.

Terminal window
curl http://127.0.0.1:9090/metrics
curl http://127.0.0.1:9090/metrics/prometheus

Troubleshooting

Connection Issues

Terminal window
# Check proxy health
curl http://localhost:9090/health
# Check pool stats
curl http://localhost:9090/pools
# Check node status
curl http://localhost:9090/nodes

Pool Exhaustion

Symptoms: connection acquire timeout errors

Solutions:

  1. Increase max_pool_size
  2. Switch to transaction or statement mode once DISCARD ALL is available in Lite (until then these modes close a backend connection on every release)
  3. Reduce application connection hold time
  4. Scale horizontally with multiple proxy instances

Query Routing Issues

Terminal window
# Enable debug logging and watch the routing decisions in the log
heliosdb-proxy --config config.toml --log-level debug
# Check which node is the current primary and which nodes are healthy
curl http://localhost:9090/topology

See Also