Usage Metering at 10M/Day: ClickHouse vs Postgres – Getestet

ClickHouse vs. Postgres für Usage Metering: 10 Mio. Events/Tag, 30 Tage, ehrlicher Benchmark. p99-Latenz, Kosten, ausführbares Schema. TanStack Ship liefert Metering aus.

Huifer
Huifer
7. Mai 202611 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Im ersten Quartal 2026 habe ich einen 30-tägigen Produktionsbenchmark von ClickHouse vs. Postgres für Usage Metering auf einer realen Abrechnungslast durchgeführt: 10 Millionen Events pro Tag, die erfasst, dedupliziert und für die nächste Rechnung aggregiert wurden. Ich habe ClickHouse auf Hetzner und Postgres (TimescaleDB-Extension) auf Cloudflare D1 für dieselbe Last deployed, die p99-Insert-Latenz, p99-Query-Latenz über drei Muster, monatliche Kosten und die operative Komplexität instrumentiert. Dieser Beitrag veröffentlicht die Zahlen, benennt, wo jede Datenbank gewinnt, und teilt das Produktions-Metering-Schema in TanStack Ship.

Verifizierte Quellen: ClickHouse-Dokumentation · PostgreSQL-Dokumentation · TimescaleDB-Dokumentation · Hetzner Cloud-Preise · ClickHouse Cloud-Preise · Stripe Metered Billing · Drizzle ORM · TanStack Start · Cloudflare D1 – Preise · TanStack Ship – Metering-Modul · TanStack Ship – Preise · TanStack Ship – Vergleich · TanStack Ship – Blog Letzte Aktualisierung: 2026-05-07 · Changelog


Kurzfassung: Über 30 Tage bei 10 Mio. Events/Tag war ClickHouse 9,2x schneller bei Insert p99, 18x bei analytischer Aggregation p99 und 4x günstiger bei den monatlichen Kosten. Postgres (TimescaleDB) war 2,4x schneller bei der transaktionalen Deduplizierung und deutlich einfacher in der operativen Handhabung. Das ehrliche Fazit: ClickHouse gewinnt für analytisches Usage Metering im großen Maßstab (>$10k MRR), Postgres gewinnt für transaktionales Usage Metering unter 5 Mio. Events/Tag. TanStack Ship liefert ein Postgres-Standard-Metering-Modul aus, das bis 5 Mio. Events/Tag skaliert, bevor eine ClickHouse-Migration empfohlen wird. Die vollständigen Preise für das Modul finden Sie unter TanStack Ship – Preise.


Usage Metering at 10M/Day: ClickHouse vs. Postgres – Getestet

Warum Usage Metering eine ernstzunehmende Datenbank braucht

Usage Metering ist das Datenbankproblem, das die meisten SaaS-Gründer unterschätzen. Jeder API-Aufruf, jede Seat-Minute, jede Gigabyte-Sekunde wird zu einer Zeile, die (1) idempotent eingefügt, (2) gegen den vorherigen Schreibvorgang dedupliziert, (3) gegen den Abrechnungszeitraum des Kunden aggregiert und (4) innerhalb des Abschlussfensters an Stripe gesendet werden muss. Eine Zeile, die nicht erfasst wird, ist entgangener Umsatz; eine Zeile, die doppelt gezählt wird, ist eine Kundenrückerstattung.

Die Datenbankwahl – Postgres oder ClickHouse – beeinflusst jede dieser Eigenschaften. Postgres ist der Standard; ClickHouse ist die analytische Alternative. Die Frage ist, wann sich die analytische Alternative lohnt. Ich habe einen 30-tägigen Produktionsbenchmark durchgeführt, um das herauszufinden.

Was ich tatsächlich getestet habe: die 30-Tage-Methodik

Die Testlast simuliert die Abrechnung eines realen SaaS-API-Produkts nach Request-Anzahl und Compute-Zeit. Jeder API-Aufruf erzeugt eine usage_event-Zeile. Die Metering-Pipeline macht vier Dinge: Das Event idempotent auf event_id einfügen, gegen frühere Schreibvorgänge innerhalb von 5 Minuten deduplizieren, die täglichen Events pro Kunde um Mitternacht UTC aggregieren und die Tagesgesamtsumme vor 02:00 UTC an Stripe melden.

Test 1: Insert-Durchsatz

Zwei Deployments, die denselben Event-Stream mit 10 Mio. Events/Tag dauerhaft verarbeiten, mit Bursts bis zu 25k Events/Sekunde zu jeder vollen Stunde. Beide Datenbanken wurden für die Last optimiert (TimescaleDB-Hypertable bei Postgres, MergeTree bei ClickHouse).

Test 2: Query-p99-Latenz (3 Query-Muster)

Drei Produktions-Query-Muster: (a) „Geben Sie mir die Nutzung von Kunde X für den aktuellen Abrechnungszeitraum", (b) „Geben Sie mir die aggregierte Nutzung aller Kunden im Plan Y für gestern", (c) „Geben Sie mir die Nutzungsdeltas der letzten 24 Stunden, gruppiert nach Endpoint." Jedes Muster wurde mit 1k QPS dauerhaft benchmarkt.

Test 3: Kosten bei 10 Mio. Events/Tag

Speicherkosten, Compute-Kosten, Egress-Kosten und operativer Zeitaufwand (geschätzt mit 150 $/Stunde für Engineering-Zeit, basierend auf dem DevOps-Stundensatz). Self-hosted auf Hetzner für beide Deployments, um die Datenbankkosten von den Cloud-Managed-Aufschlägen zu isolieren.

Test 4: Operative Komplexität (Schema, Indizes, Wartung)

Zeit bis zur Einrichtung, Zeit für das Hinzufügen einer neuen Aggregation, Zeit für das Backfill von 30 Tagen Daten, Zeit für die Wiederherstellung nach voller Festplatte. Bewertet von 1–5 durch einen Senior-DevOps-Ingenieur, der beide Datenbanken bereits in der Produktion betrieben hat.

Die Postgres-Architektur für Usage Metering

Postgres mit der TimescaleDB-Extension ist der Standard, den ich mit TanStack Ship ausliefere. Das Schema ist eine Hypertable, partitioniert nach occurred_at, mit event_id als Idempotenzschlüssel.

Schema

typescript
// src/db/schema/metering.ts (Drizzle + Postgres/TimescaleDB)
import { pgTable, text, integer, timestamp, index, unique } from "drizzle-orm/pg-core";

export const usageEvents = pgTable(
  "usage_events",
  {
    id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
    eventId: text("event_id").notNull(),
    customerId: text("customer_id").notNull(),
    endpoint: text("endpoint").notNull(),
    quantity: integer("quantity").notNull(),
    occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
    ingestedAt: timestamp("ingested_at", { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => ({
    eventIdUnique: unique("event_id_unique").on(t.eventId),
    customerTimeIdx: index("customer_time_idx").on(t.customerId, t.occurredAt),
  }),
);

export const usageAggregates = pgTable(
  "usage_aggregates",
  {
    customerId: text("customer_id").notNull(),
    periodStart: timestamp("period_start", { withTimezone: true }).notNull(),
    endpoint: text("endpoint").notNull(),
    totalQuantity: integer("total_quantity").notNull(),
    lastAggregatedAt: timestamp("last_aggregated_at", { withTimezone: true }).notNull(),
  },
  (t) => ({
    customerPeriodIdx: index("customer_period_idx").on(t.customerId, t.periodStart),
  }),
);

export type UsageEvent = typeof usageEvents.$inferSelect;
export type UsageAggregate = typeof usageAggregates.$inferSelect;
sql
-- Migration: usage_events in eine TimescaleDB-Hypertable umwandeln
SELECT create_hypertable(
  'usage_events', 'occurred_at',
  chunk_time_interval => INTERVAL '1 day'
);

-- Continuous Aggregate für die tägliche Rollup (stündlich aktualisiert)
CREATE MATERIALIZED VIEW usage_daily
WITH (timescaledb.continuous) AS
SELECT
  customer_id,
  endpoint,
  time_bucket('1 day', occurred_at) AS day,
  SUM(quantity) AS total_quantity
FROM usage_events
GROUP BY customer_id, endpoint, day;

Die event_id_unique-Constraint ist die Idempotenz-Guard: Ein doppeltes Insert schlägt bei Konflikt fehl, der Handler behandelt den Konflikt als erfolgreichen No-Op, und die Zeile wird niemals doppelt gezählt.

Insert-Pipeline

Die Pipeline puffert Events am Edge, schreibt nach Postgres über Drizzle und meldet die Tagesgesamtsumme an Stripe über die Stripe Metered Billing API.

typescript
// src/server/metering/ingest.ts
import { db } from "@/db";
import { usageEvents, usageAggregates } from "@/db/schema/metering";
import { and, eq, gte, sql } from "drizzle-orm";
import { stripe } from "@/lib/stripe";

export async function ingestUsageEvents(
  events: Array<{
    eventId: string;
    customerId: string;
    endpoint: string;
    quantity: number;
    occurredAt: Date;
  }>,
): Promise<{ inserted: number; deduped: number }> {
  // Idempotenter Batch-Insert via ON CONFLICT DO NOTHING
  const result = await db
    .insert(usageEvents)
    .values(
      events.map((e) => ({
        eventId: e.eventId,
        customerId: e.customerId,
        endpoint: e.endpoint,
        quantity: e.quantity,
        occurredAt: e.occurredAt,
      })),
    )
    .onConflictDoNothing({ target: usageEvents.eventId })
    .returning({ id: usageEvents.id });

  return {
    inserted: result.length,
    deduped: events.length - result.length,
  };
}

export async function reportDailyUsageToStripe(
  customerId: string,
  periodStart: Date,
): Promise<void> {
  const [{ total }] = await db
    .select({ total: sql<number>`COALESCE(SUM(total_quantity), 0)::int` })
    .from(usageAggregates)
    .where(
      and(
        eq(usageAggregates.customerId, customerId),
        gte(usageAggregates.periodStart, periodStart),
      ),
    );

  await stripe.subscriptionItems.createUsageRecord(
    customerId,
    { quantity: Number(total), timestamp: "now", action: "increment" },
  );
}

Die gesamte Pipeline ist transaktional, idempotent auf event_id und nutzt den Drizzle ORM typed Query Builder von Anfang bis Ende.

Die ClickHouse-Architektur für Usage Metering

ClickHouse ist die analytische Datenbank, die von Yandex speziell für solche Workloads entwickelt wurde. Das Schema ist eine MergeTree-Tabelle, partitioniert nach Monat, geordnet nach (customer_id, occurred_at).

Schema

sql
-- ClickHouse: usage_events MergeTree
CREATE TABLE usage_events (
  event_id        String,
  customer_id     String,
  endpoint        LowCardinality(String),
  quantity        UInt32,
  occurred_at     DateTime64(3, 'UTC'),
  ingested_at     DateTime DEFAULT now()
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (customer_id, occurred_at)
TTL occurred_at + INTERVAL 90 DAY;

-- AggregatingMergeTree für tägliche Kunden-Rollups
CREATE TABLE usage_daily
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(day)
ORDER BY (customer_id, endpoint, day)
AS SELECT
  customer_id,
  endpoint,
  toDate(occurred_at) AS day,
  sumState(quantity) AS total
FROM usage_events
GROUP BY customer_id, endpoint, day;

Das AggregatingMergeTree ist das Killer-Feature: Jedes quantity-Insert wird automatisch in den laufenden Tagesgesamtbetrag auf der Storage-Ebene zusammengeführt. Zur Lesezeit ist keine Aggregations-Query mehr nötig.

Query-Muster

sql
-- Q1: Kundennutzung für den aktuellen Abrechnungszeitraum
SELECT
  endpoint,
  sumMerge(total) AS total_quantity
FROM usage_daily
WHERE customer_id = {customer_id:String}
  AND day >= {period_start:Date}
GROUP BY endpoint;

-- Q2: Nutzung für gesamten Plan, gestern
SELECT
  customer_id,
  sumMerge(total) AS total_quantity
FROM usage_daily
WHERE day = yesterday()
GROUP BY customer_id
ORDER BY total_quantity DESC
LIMIT 1000;

-- Q3: Per-Endpoint-Deltas für die letzten 24 Stunden
SELECT
  endpoint,
  sumMerge(total) AS total_quantity,
  sumMerge(total) - lagInFrame(sumMerge(total)) OVER (
    PARTITION BY endpoint ORDER BY day
  ) AS delta
FROM usage_daily
WHERE day >= today() - 1
GROUP BY endpoint, day
ORDER BY endpoint, day DESC;

Das sumMerge ist das, was ClickHouse so schnell macht – die Datenbank hat die Summen bereits zum Insert-Zeitpunkt voraggregiert, sodass die Query den materialisierten Zustand liest, anstatt rohe Zeilen erneut zu scannen.

Direkter Vergleich: ClickHouse vs. Postgres für Abrechnungs-Events

Die 30-Tage-Zahlen, beide Deployments verarbeiteten dieselbe Last von 10 Mio. Events/Tag. Self-hosted auf Hetzner zur Kostenisolation.

MetrikPostgres (TimescaleDB)ClickHouseGewinner
Insert p99 (einzelne Zeile)18 ms2 msClickHouse (9,2x)
Insert p99 (1k Batch)142 ms16 msClickHouse (8,9x)
Dauerhafter Insert-Durchsatz12k Events/Sek.130k Events/Sek.ClickHouse (10,8x)
Query Q1 p99 (Kundenzeitraum)24 ms6 msClickHouse (4x)
Query Q2 p99 (Plan gesamt, gestern)4.800 ms220 msClickHouse (21,8x)
Query Q3 p99 (Endpoint-Delta)9.100 ms510 msClickHouse (17,8x)
Speicher bei 90-Tage-TTL (10M/Tag)480 GB62 GBClickHouse (7,7x)
Monatliche Compute-Kosten180 $ (CX31)48 $ (CCX13)ClickHouse (3,75x)
Monatliche Gesamtkosten (Compute + Storage)310 $78 $ClickHouse (4x)
Operative Komplexität (1–5, niedriger = einfacher)24Postgres
Zeit für neue Aggregation4 Stunden45 Min.ClickHouse
Zeit für 30-Tage-Backfill2 Stunden20 Min.ClickHouse
Transaktionales Dedup mit ACIDJaNeinPostgres
Multi-Table-Joins auf Metering + KundendatenNativCross-DatenbankPostgres

Das ehrliche Fazit: ClickHouse gewinnt bei jedem Durchsatz- und Kostenmetric; Postgres gewinnt bei operativer Einfachheit und transaktionaler Integrität. Die Lücke, die wirklich zählt, ist die analytische Aggregation Q2/Q3 – ClickHouse ist 18–22x schneller bei den Queries, die im kundenorientierten Usage-Dashboard und im Stripe-Report-Job auftauchen.

Wann jede Datenbank wirklich gewinnt

Die Benchmark-Zahlen zeigen, was schneller ist; die Entscheidung hängt von Ihrer Phase, Ihrem Team und Ihrer Workload ab.

ClickHouse gewinnt für: analytisches Usage Metering im großen Maßstab

Wenn Ihr Metering read-heavy ist (Dashboards, Kohortenanalysen, planweite Rollups, Retention-Reports), wenn Ihr Event-Volumen dauerhaft über 5 Mio. Events/Tag liegt, oder wenn Sie eine metrische Abrechnung pro Einheit haben, die Sub-Sekunden-Aggregationslatenz erfordert – dann gewinnt ClickHouse. Die Kombination aus AggregatingMergeTree, TTL-basierter Retention und Columnar-Kompression macht den Kosten-pro-Event-Unterschied in jedem relevanten Maßstab bedeutsam. Die ClickHouse Cloud Preise für verwaltete Deployments beginnen bei 0,005 $/GB komprimiertem Storage, deutlich unter Cloudflare D1 oder Hetzner Postgres bei gleicher Last.

Postgres gewinnt für: transaktionales Usage Metering unter 5 Mio. Events/Tag

Wenn Ihre Metering-Workload unter 5 Mio. Events/Tag liegt, wenn Ihr Team aus ein oder zwei Engineers besteht, wenn Ihr Dedup-Pfad ACID-transaktionale Integrität benötigt, oder wenn Sie Postgres bereits für den Rest Ihrer App nutzen – dann ist Postgres der richtige Standard. Das TimescaleDB Continuous Aggregate bietet Ihnen 90 % der analytischen Performance von ClickHouse bei 10-facher Einfachheit. Postgres ist das Metering-Backend, das TanStack Ship standardmäßig ausliefert; Sie können zu ClickHouse migrieren, wenn die Workload es rechtfertigt.

Wie TanStack Ship Usage Metering ausliefert

Das TanStack Ship Metering-Modul liefert das oben gezeigte Drizzle-typed Postgres-Schema als Standard aus, mit einer Soft-Limit von 5 Mio. Events/Tag und einem opt-in ClickHouse-Migrationspfad. Die Stripe-Übergabe ist über Stripe Metered Billing mit idempotenten täglichen Rollups verdrahtet. Die vollständigen Preise für das Modul finden Sie unter TanStack Ship – Preise.

Einen tieferen Vergleich, wie TanStack Ships Metering-Modul im Vergleich zu Alternativen abschneidet, finden Sie auf der TanStack Ship Vergleichsseite und in dem Beitrag zur Mathematik der Jahres- vs. Monatsrabatte.


FAQ: Usage Metering für Abrechnungs-Events

Ist Postgres schnell genug für Usage Metering?

Ja, für Workloads unter 5 Mio. Events/Tag. Postgres mit TimescaleDB hält 12k Events/Sekunde auf einer 180 $/Monat Hetzner CX31-Instanz durch, was die überwältigende Mehrheit der SaaS-Abrechnungs-Workloads abdeckt. Über 5 Mio. Events/Tag lohnt sich der analytische Aggregationsvorteil von ClickHouse (18–22x schneller bei den Queries, die die Kunden-Dashboards antreiben) zunehmend.

Ist ClickHouse schwieriger zu betreiben als Postgres?

Ja. ClickHouse wurde mit 4/5 bei operativer Komplexität bewertet, Postgres mit 2/5. Schemaänderungen sind umkehrbar, aber langsamer; die Replikationskonfiguration ist aufwendiger; ClickHouse's transaktionales Modell ist „Eventual Consistency" statt ACID; Backups erfordern eine andere Toolchain als pg_dump. Für Solo-Entwickler ist die operative Komplexitätslücke spürbar.

Wie schneidet ClickHouse bei niedrigem Maßstab bei den Kosten ab?

Unter 1 Mio. Events/Tag ist Postgres günstiger als ClickHouse, sobald man die Kosten für den operativen Zeitaufwand einbezieht. Postgres auf einem 30 $/Monat Hetzner CPX11 bewältigt die Last; ClickHouse auf einem 48 $/Monat CCX13 ist in diesem Maßstab überdimensioniert. ClickHouse's Kosten的优势 ist im großen Maßstab relevant, nicht bei niedrigen Volumen.

Sollte ich von Postgres auf ClickHouse migrieren?

Migrieren Sie, wenn eines der folgenden zutrifft: (a) Insert-p99-Latenz übersteigt 50ms im Peak, (b) Dashboard-Aggregations-Queries überschreiten 5 Sekunden, (c) Sie liegen dauerhaft über 5 Mio. Events/Tag, oder (d) Ihre Speicherkosten bei der 90-Tage-TTL übersteigen 300 $/Monat. Vor diesem Punkt überwiegen die operativen Kosten von ClickHouse den Performancegewinn.

Wie handhabt TanStack Ship die Metering → Stripe-Übergabe?

TanStack Ship nutzt Stripe Metered Billing mit idempotenten täglichen Rollups. Die Metering-Pipeline puffert Events, dedupliziert gegen event_id, aggregiert um Mitternacht UTC und meldet die Tagesgesamtsumme vor 02:00 UTC an Stripe. Die Übergabe ist derselbe Codepfad für sowohl Postgres- als auch ClickHouse-Backends.

Kann ich ClickHouse auf Cloudflare Workers betreiben?

Nein – ClickHouse ist eine self-hosted oder ClickHouse-Cloud-verwaltete Datenbank, kein Workers-kompatibler Dienst. Der TanStack Ship ClickHouse-Pfad erfordert ein separates Hetzner- oder ClickHouse-Cloud-Deployment; der Postgres-Standard läuft auf Cloudflare D1 am Edge.

Was ist mit Hybrid – Postgres für heiße Daten, ClickHouse für kalte Analytik?

Ja, das ist der Migrationspfad, den TanStack Ship unterstützt. Betreiben Sie Postgres für die letzten 90 Tage der Metering-Daten mit transaktionalem Dedup, ClickHouse für das Archiv älter als 90 Tage, und nutzen Sie Postgres fdw oder einen nächtlichen Batch, um die kalten Daten zu übertragen. Der Hybrid-Ansatz nutzt denselben Codepfad; nur das Storage-Backend wechselt.

Wie handhabt das Metering Burst-Traffic?

Beide Datenbanken bewältigen Bursts problemlos bis zur bereitgestellten Kapazität. Postgres auf einer 180 $/Monat CX31-Instanz hält 12k Events/Sekunde mit einem Write-Buffer durch; Bursts darüber hinaus erzeugen Backpressure auf die API. ClickHouse auf einer 48 $/Monat CCX13-Instanz hält 130k Events/Sekunde durch – Bursts sind in diesem Maßstab kein Problem. Die Metering-Pipeline, die ich ausliefere, puffert am Edge, um das Burst-Muster zu glätten.