TimescaleDB on a VPS: PostgreSQL for Time-Series Data, IoT Metrics, and Application Monitoring

TimescaleDB on a VPS: PostgreSQL for Time-Series Data, IoT Metrics, and Application Monitoring

TimescaleDB is a PostgreSQL extension that transforms it into a high-performance time-series database — using hypertables (automatically partitioned by time) to achieve 10–100× faster time-range queries and inserts compared to standard PostgreSQL tables. It handles IoT sensor data, application performance metrics, financial tick data, and event streams while retaining full SQL compatibility. No new query language, no new tooling — just PostgreSQL that’s optimized for time-series patterns.

TimescaleDB vs InfluxDB vs ClickHouse for Time-Series

Factor TimescaleDB InfluxDB ClickHouse
Query language SQL (PostgreSQL) Flux / InfluxQL SQL (dialect)
Joins & relations Full PostgreSQL joins Limited Possible
Existing PostgreSQL Extension — same DB Separate service Separate service
Write throughput High (100K+ inserts/s) Very high Very high (bulk)
Aggregation queries Fast with hypertables Fast Fastest
Best for SQL-first, PostgreSQL shop Metrics & monitoring Analytics & logs

Step 1: Install TimescaleDB

<code"># Add TimescaleDB repository
sudo apt install -y gnupg postgresql-common apt-transport-https lsb-release
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh

# Install TimescaleDB for PostgreSQL 16
sudo apt install -y timescaledb-2-postgresql-16

# Run TimescaleDB tune (optimizes PostgreSQL config for time-series workloads)
sudo timescaledb-tune --quiet --yes

sudo systemctl restart postgresql

# Verify installation
sudo -u postgres psql -c "SELECT installed_version FROM pg_available_extensions WHERE name = 'timescaledb';"

Step 2: Enable TimescaleDB Extension

<code">sudo -u postgres psql
<code">CREATE DATABASE metrics;
\c metrics

-- Enable TimescaleDB extension
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- Verify
SELECT extversion FROM pg_extension WHERE extname = 'timescaledb';
\q

Step 3: Create Hypertables

<code">sudo -u postgres psql metrics
<code">-- IoT sensor readings table
CREATE TABLE sensor_readings (
    time        TIMESTAMPTZ NOT NULL,
    device_id   TEXT NOT NULL,
    location    TEXT,
    temperature DOUBLE PRECISION,
    humidity    DOUBLE PRECISION,
    pressure    DOUBLE PRECISION,
    battery     DOUBLE PRECISION
);

-- Convert to hypertable partitioned by time (7-day chunks)
SELECT create_hypertable(
    'sensor_readings',
    by_range('time', INTERVAL '7 days')  -- 7-day partitions
);

-- Application metrics table
CREATE TABLE app_metrics (
    time        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    service     TEXT NOT NULL,
    metric_name TEXT NOT NULL,
    value       DOUBLE PRECISION,
    tags        JSONB DEFAULT '{}'
);

SELECT create_hypertable('app_metrics', by_range('time'));

-- Create indexes for common query patterns
CREATE INDEX ON sensor_readings (device_id, time DESC);
CREATE INDEX ON app_metrics (service, metric_name, time DESC);

Step 4: Insert Time-Series Data

<code">pip install psycopg2-binary
<code">import psycopg2
from psycopg2.extras import execute_values
from datetime import datetime, timedelta
import random

conn = psycopg2.connect("postgresql://postgres:password@localhost/metrics")

def insert_sensor_batch(readings: list[dict]):
    """Insert multiple sensor readings efficiently."""
    with conn.cursor() as cur:
        execute_values(cur, """
            INSERT INTO sensor_readings
                (time, device_id, location, temperature, humidity, pressure, battery)
            VALUES %s
        """, [(
            r['time'], r['device_id'], r['location'],
            r['temperature'], r['humidity'], r['pressure'], r['battery']
        ) for r in readings])
    conn.commit()

def record_metric(service: str, metric: str, value: float, tags: dict = None):
    """Record a single application metric."""
    with conn.cursor() as cur:
        cur.execute("""
            INSERT INTO app_metrics (service, metric_name, value, tags)
            VALUES (%s, %s, %s, %s)
        """, (service, metric, value, psycopg2.extras.Json(tags or {})))
    conn.commit()

# Generate sample IoT data
readings = []
now = datetime.now()
for i in range(1000):
    readings.append({
        'time': now - timedelta(minutes=i),
        'device_id': f'sensor-{random.randint(1, 5)}',
        'location': random.choice(['warehouse-a', 'warehouse-b', 'office']),
        'temperature': 20 + random.uniform(-5, 10),
        'humidity': 50 + random.uniform(-20, 20),
        'pressure': 1013 + random.uniform(-10, 10),
        'battery': random.uniform(0, 100),
    })

insert_sensor_batch(readings)
record_metric('api-server', 'request_duration_ms', 145.3, {'endpoint': '/api/users'})

Step 5: Time-Series Queries

<code">-- Average temperature per device per hour (last 24 hours)
SELECT
    time_bucket('1 hour', time) AS bucket,
    device_id,
    AVG(temperature) AS avg_temp,
    MAX(temperature) AS max_temp,
    MIN(temperature) AS min_temp,
    COUNT(*) AS readings
FROM sensor_readings
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY bucket, device_id
ORDER BY bucket DESC, device_id;

-- Moving average (smooth noisy sensor data)
SELECT
    time,
    device_id,
    temperature,
    AVG(temperature) OVER (
        PARTITION BY device_id
        ORDER BY time
        ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
    ) AS moving_avg_5
FROM sensor_readings
WHERE device_id = 'sensor-1'
  AND time > NOW() - INTERVAL '6 hours'
ORDER BY time;

-- Devices with anomalous readings (outside 3 standard deviations)
SELECT
    device_id,
    time,
    temperature,
    AVG(temperature) OVER (PARTITION BY device_id) AS mean,
    STDDEV(temperature) OVER (PARTITION BY device_id) AS stddev
FROM sensor_readings
WHERE time > NOW() - INTERVAL '7 days'
HAVING ABS(temperature - AVG(temperature) OVER (PARTITION BY device_id)) >
       3 * STDDEV(temperature) OVER (PARTITION BY device_id);

-- Continuous aggregate: pre-computed hourly averages
CREATE MATERIALIZED VIEW hourly_sensor_stats
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time) AS bucket,
    device_id,
    AVG(temperature) AS avg_temp,
    AVG(humidity) AS avg_humidity
FROM sensor_readings
GROUP BY bucket, device_id;

-- Query the materialized view (much faster than raw table)
SELECT * FROM hourly_sensor_stats
WHERE bucket > NOW() - INTERVAL '7 days'
ORDER BY bucket DESC;

Step 6: Data Retention Policy

<code">-- Auto-delete data older than 90 days (saves disk, keeps recent data)
SELECT add_retention_policy('sensor_readings', INTERVAL '90 days');

-- For app_metrics, keep 30 days of raw data
SELECT add_retention_policy('app_metrics', INTERVAL '30 days');

-- But keep the continuous aggregate (hourly summaries) longer:
SELECT add_retention_policy('hourly_sensor_stats', INTERVAL '2 years');

-- View active policies
SELECT * FROM timescaledb_information.jobs;

Step 7: Compression

<code">-- Enable compression for data older than 7 days (reduces storage 90-95%)
ALTER TABLE sensor_readings SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'device_id',
    timescaledb.compress_orderby = 'time DESC'
);

SELECT add_compression_policy('sensor_readings', INTERVAL '7 days');

-- Check compression ratio
SELECT
    chunk_schema, chunk_name,
    pg_size_pretty(before_compression_total_bytes) AS before,
    pg_size_pretty(after_compression_total_bytes) AS after
FROM chunk_compression_stats('sensor_readings');
-- Typical output: 10 MB → 0.5 MB (95% compression for time-series data)

Getting Started

TimescaleDB installs as a PostgreSQL extension — no additional service to run. A 2 GB Ubuntu VPS at VPS.DO handles thousands of time-series inserts per second and fast time-range queries for IoT, monitoring, and analytics use cases. Compression reduces storage requirements by 90–95% for older data, making long-term time-series retention economical on VPS disk.

Conclusion

TimescaleDB transforms standard PostgreSQL into a time-series database with 10–100× faster time-range queries, automatic chunk pruning, 90–95% compression for older data, and continuous aggregates for pre-computed summaries. For teams already running PostgreSQL, adding TimescaleDB is a single CREATE EXTENSION command — no new service, no new query language, no new operational burden. Ideal for IoT sensor pipelines, application performance monitoring, and financial data storage where SQL expressiveness and relational capabilities matter alongside time-series performance.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!