High‑performance REST API for streaming, storing, and querying time‑series sensor data. Built with FastAPI, PostgreSQL + TimescaleDB extension (hypertables), containerised with Docker, and deployed to local Kubernetes (Minikube) for orchestration simulation.
- Stream sensor data – single or batch ingestion (
POST /sensor_data/{sensor_id}) - Sensor management – create new sensors (
POST /sensors) - Time‑series analytics – daily aggregates: average, min, max, median, interquartile range (IQR) using TimescaleDB’s
time_bucketand percentile functions - Async & high performance – asyncpg connection pooling, FastAPI async endpoints
- Auto‑generated API documentation – Swagger UI and ReDoc
- Containerised – Docker image available
- Kubernetes ready – Minikube manifests included (Deployment, Service, Secret)
| Layer | Technology |
|---|---|
| API Framework | FastAPI (Python) |
| ASGI Server | Uvicorn |
| Database | PostgreSQL 15 + TimescaleDB extension (via Neon serverless) |
| DB Driver | asyncpg (asynchronous) |
| Environment | Python-dotenv |
| Logging | Loguru |
| Container | Docker |
| Orchestration | Kubernetes (Minikube for local simulation) |
The sensor data table is stored as a TimescaleDB hypertable, which automatically partitions data into time‑based chunks for fast inserts and queries. The API uses a connection pool to handle many concurrent requests.
- Python 3.12+
uv(fast Python package manager) – install withpip install uv- A Neon PostgreSQL account (free tier)
- Docker (for containerisation)
- Minikube and kubectl (for Kubernetes simulation)
- Optional:
httpiefor testing (uv pip install httpie)
-
Clone the repository
git clone https://github.com/yourusername/timescale_fastapi.git cd timescale_fastapi -
Create virtual environment and install dependencies
uv venv source .venv/bin/activate # Linux/macOS uv add python-dotenv asyncpg loguru fastapi uvicorn requests
-
Configure environment variables
Create a.envfile in the project root:DATABASE_URL=postgresql://user:password@host.neon.tech/neondb?sslmode=requireReplace with your Neon connection string (find it in Neon Console → Connect).
-
Initialize the database
Run the following SQL in Neon’s SQL Editor:CREATE EXTENSION IF NOT EXISTS timescaledb; CREATE TABLE IF NOT EXISTS sensors ( sensor_id SERIAL PRIMARY KEY, sensor_type VARCHAR(50) NOT NULL, description VARCHAR(255), location VARCHAR(255) ); CREATE TABLE IF NOT EXISTS sensor_data ( sensor_id INT REFERENCES sensors(sensor_id), value FLOAT NOT NULL, time TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY(sensor_id, time) ); SELECT create_hypertable('sensor_data', 'time');
(Optionally insert sample data – see
docs/sample_data.sqlin the repository) -
Run the API locally
uvicorn src.main:app --host 0.0.0.0 --port 8080 --reload
Visit
http://localhost:8080/docsfor interactive documentation.
Build the Docker image (the .env file is not baked into the image):
docker build -t timescale-fastapi .Run the container, injecting the database URL from your host’s .env file:
docker run -d --name sensor-api -p 8080:8080 --env-file .env timescale-fastapiCheck logs:
docker logs sensor-apiStop and remove:
docker stop sensor-api && docker rm sensor-apiFor local orchestration simulation, we use Minikube. The database credentials are passed via a Kubernetes Secret.
minikube start --driver=dockereval $(minikube docker-env)
docker build -t timescale-fastapi:latest .minikube kubectl -- create secret generic sensor-api-secret \
--from-literal=DATABASE_URL="postgresql://user:password@host.neon.tech/neondb?sslmode=require"Replace with your actual Neon connection string.
The repository includes k8s/deployment.yaml and k8s/service.yaml. Apply them:
minikube kubectl -- apply -f k8s/deployment.yaml
minikube kubectl -- apply -f k8s/service.yamlminikube kubectl -- get pods
minikube kubectl -- get servicesUse port‑forwarding to reach the service from your local machine:
minikube kubectl -- port-forward service/sensor-api-service 8080:8080Then open http://localhost:8080/docs in your browser.
Alternatively, use Minikube’s tunnel:
minikube service sensor-api-service --urlminikube delete # deletes the entire clusterPOST /sensors
Request body:
{
"sensor_type": "temperature",
"description": "Living room temperature sensor",
"location": "Living Room"
}Response:
{
"sensor_id": 3,
"message": "Sensor created successfully."
}Test with httpie:
http POST http://localhost:8080/sensors sensor_type="temperature" description="Living room sensor" location="Living Room"POST /sensor_data/{sensor_id}
Path parameter: sensor_id (integer)
Request body (single reading):
{
"value": 23.5,
"timestamp": "2025-05-01T14:29:00"
}Response:
{
"message": "Sensor data streamed successfully."
}Example:
http POST http://localhost:8080/sensor_data/3 value:=23.5 timestamp="2025-05-01T14:29:00"POST /sensor_data/{sensor_id}
Request body (batch):
{
"data": [
{"value": 22.5, "timestamp": "2025-05-01T14:30:00"},
{"value": 22.7, "timestamp": "2025-05-01T14:31:00"}
]
}Response same as single.
Example:
http POST http://localhost:8080/sensor_data/3 data:='[{"value": 22.5, "timestamp": "2025-05-01T14:30:00"}, {"value": 22.7, "timestamp": "2025-05-01T14:31:00"}]'GET /daily_avg/{sensor_id}
Path parameter: sensor_id
Response:
[
{
"day": "2025-05-01",
"sensor_id": 3,
"avg_value": 22.6,
"min_value": 22.5,
"max_value": 22.7,
"reading_count": 2,
"median_value": 22.6,
"iqr_value": 0.2
}
]Example:
http GET http://localhost:8080/daily_avg/3sensors– metadata for each sensor device.sensor_data– hypertable partitioned bytime. Primary key is(sensor_id, time)– optimised for time‑range queries on a single sensor.
TimescaleDB provides:
time_bucket('1 day', time)– aggregate data into daily buckets.percentile_cont()– compute median and IQR directly in SQL.
timescale_fastapi/
├── .env # Database credentials (ignored by git)
├── .gitignore
├── README.md
├── Dockerfile
├── .dockerignore
├── pyproject.toml # Dependencies (managed by uv)
├── uv.lock
├── k8s/
│ ├── deployment.yaml
│ └── service.yaml
├── src/
│ ├── database/
│ │ └── postgres.py # Connection pool & lifecycle
│ ├── models/
│ │ └── sensor_models.py # Pydantic schemas
│ ├── routes/
│ │ └── sensor_routes.py # API endpoints
│ └── main.py # FastAPI app entrypoint
└── tests/ # (future)
- Run locally:
uvicorn src.main:app --reload - Run with Docker:
docker run -p 8080:8080 --env-file .env timescale-fastapi - Run on Minikube: follow Kubernetes steps above
- Interactive docs:
http://localhost:8080/docs
MIT
- Inspired by the official TimescaleDB + FastAPI tutorial.
- Built with
uv,asyncpg, Neon free tier, Docker, and Minikube.