Módulo 7: Production con Pinecone — la migración de "demo funcional" a "servicio 24/7"
Benchmarking y Costos: ChromaDB vs Pinecone
Descripción de la cápsula
Hasta aquí tienes un sistema RAG en Pinecone con namespaces, filtros tipados y aislamiento multi-tenant. Pero antes de declarar la migración exitosa y cerrar el ticket, necesitas responder una pregunta que tu jefe, tu CFO o tu cofundador van a hacer: ¿esto realmente vale lo que cuesta?
Migrar sin benchmarks es una decisión basada en opinión. "Pinecone es más rápido" sin números es marketing. "ChromaDB se sentía lento" sin medirlo es percepción. Las decisiones de infraestructura sostenibles se toman con datos: latencias percentiladas, throughput sostenido, costo mensual estimado y degradación bajo carga.
En esta cápsula vas a construir un benchmark reproducible que mida ambos sistemas bajo las mismas condiciones, calcular costos realistas con la calculadora de Pinecone serverless, y producir un informe ejecutivo de una página que cierre la discusión "¿migramos o no?" con evidencia, no con opinión.
El entregable final es un script que puedes correr en cualquier momento (cuando los precios cambien, cuando crezca el volumen, cuando aparezca un competidor) para revalidar la decisión.
Qué medir y por qué
Cuatro dimensiones cuentan para la decisión, en orden de importancia:
| Métrica | Por qué importa | Cómo se mide |
|---|---|---|
| p95 / p99 latencia | El 5% peor de tus usuarios define la percepción del producto. Promedio mienten. | Recolectar 500+ queries, ordenar, tomar percentil |
| Throughput sostenido | Define cuántos usuarios concurrentes puedes servir sin degradar | QPS bajo carga durante 5+ minutos |
| Costo mensual proyectado | Decide si el ROI es real | Calculadora oficial × volumen esperado a 12 meses |
| Calidad (recall@k) | Ningún sistema vale si devuelve resultados peores | Comparar matches contra ground truth |
Anti-patrón clásico: medir solo latencia promedio (avg). Tu p99 puede ser 4× el promedio y la mediana se ve "buena" mientras un 1% de tus usuarios sufre 3 segundos de espera. Promedio para el dashboard de la junta; percentiles para decisiones de ingeniería.
Diseño del benchmark reproducible
Un benchmark que importa cumple cinco requisitos:
- Mismo dataset indexado en ambos sistemas (mismos vectores, mismo metadata)
- Mismo conjunto de queries (idealmente queries reales de logs, no sintéticas)
- Misma configuración de retrieval (mismo
top_k, mismos filtros) - Mismas condiciones de red (correr desde el mismo cliente, idealmente cerca de Pinecone)
- Suficientes muestras (mínimo 500 queries; idealmente 2000+)
from dataclasses import dataclass
@dataclass
class BenchmarkConfig:
name: str
dataset_size: int
queries: list[dict]
top_k: int = 10
warmup_runs: int = 50
measure_runs: int = 1000
def __post_init__(self):
assert len(self.queries) >= self.measure_runs, "Need enough queries"
Los warmup_runs son críticos: las primeras queries pagan caches frías, conexiones TCP nuevas, JIT warmup. Descártalos.
Script de benchmark con percentiles correctos
import time
import statistics
from typing import Callable
def benchmark_retriever(
name: str,
run_query: Callable[[dict], list],
queries: list[dict],
warmup: int = 50,
) -> dict:
# Warmup descartado
for q in queries[:warmup]:
run_query(q)
latencies_ms = []
errors = 0
for q in queries[warmup:]:
start = time.perf_counter()
try:
run_query(q)
latencies_ms.append((time.perf_counter() - start) * 1000)
except Exception:
errors += 1
latencies_ms.sort()
n = len(latencies_ms)
return {
"name": name,
"samples": n,
"errors": errors,
"p50": latencies_ms[int(0.50 * n)],
"p90": latencies_ms[int(0.90 * n)],
"p95": latencies_ms[int(0.95 * n)],
"p99": latencies_ms[int(0.99 * n)],
"avg": statistics.mean(latencies_ms),
"stdev": statistics.stdev(latencies_ms) if n > 1 else 0,
}
Detalles que parecen menores pero importan:
time.perf_counter()en lugar detime.time(): monotónico, alta resolución, no afectado por NTP.- Errores se cuentan por separado, no se promedian con latencias exitosas (si Pinecone tiene 5% de errores y ChromaDB 0%, los promedios mienten).
- Multiplicar por 1000 al insertar (no al final) para evitar pérdida de precisión.
Throughput sostenido bajo concurrencia
Latencia secuencial no predice comportamiento bajo carga. Un sistema puede tener p95=200ms con 1 cliente y p95=4000ms con 50 clientes concurrentes.
import asyncio
from time import perf_counter
async def measure_throughput(
async_query: Callable,
queries: list[dict],
concurrency: int,
duration_sec: int = 300,
):
sem = asyncio.Semaphore(concurrency)
latencies = []
completed = 0
async def one_request(q):
nonlocal completed
async with sem:
start = perf_counter()
await async_query(q)
latencies.append((perf_counter() - start) * 1000)
completed += 1
deadline = perf_counter() + duration_sec
tasks = []
idx = 0
while perf_counter() < deadline:
tasks.append(asyncio.create_task(one_request(queries[idx % len(queries)])))
idx += 1
if len(tasks) >= concurrency * 10:
await asyncio.gather(*tasks)
tasks = []
if tasks:
await asyncio.gather(*tasks)
elapsed = duration_sec
return {
"concurrency": concurrency,
"qps": completed / elapsed,
"p95_ms": sorted(latencies)[int(0.95 * len(latencies))],
"completed": completed,
}
Corre este test a concurrency=[1, 10, 50, 100] y grafica QPS vs latencia. El punto donde p95 empieza a disparar es tu capacidad real.
Estimación de costos: Pinecone serverless
Pinecone serverless cobra por tres dimensiones (consulta pinecone.io/pricing para precios al día, esto es a marzo 2026):
| Dimensión | Costo aproximado | Significado |
|---|---|---|
| Storage | $0.33 / GB / mes | Vectores almacenados |
| Read units | $8.25 / millón | Cada query consume read units (variable según top_k y filtro) |
| Write units | $2.00 / millón | Cada upsert consume write units |
Aproximaciones útiles:
- 1 vector de 1536 dims con metadata pequeño ≈ 8 KB → 1M vectores ≈ 8 GB → ~$2.6/mes storage
- 1 query típica con
top_k=10≈ 1-3 read units (escala con tamaño de namespace) - 1 upsert ≈ 1 write unit por vector
def estimate_pinecone_monthly(
vectors_total: int,
avg_metadata_kb: float,
queries_per_month: int,
avg_read_units_per_query: float,
upserts_per_month: int,
prices: dict | None = None,
) -> dict:
p = prices or {
"storage_gb_month": 0.33,
"read_units_million": 8.25,
"write_units_million": 2.00,
}
storage_gb = (vectors_total * (8 + avg_metadata_kb)) / (1024 * 1024)
storage_cost = storage_gb * p["storage_gb_month"]
read_cost = (queries_per_month * avg_read_units_per_query / 1_000_000) * p["read_units_million"]
write_cost = (upserts_per_month / 1_000_000) * p["write_units_million"]
total = storage_cost + read_cost + write_cost
return {
"storage_gb": round(storage_gb, 2),
"storage_cost": round(storage_cost, 2),
"read_cost": round(read_cost, 2),
"write_cost": round(write_cost, 2),
"total_monthly_usd": round(total, 2),
}
Ejemplo realista para un SaaS B2B:
estimate_pinecone_monthly(
vectors_total=2_000_000, # 2M chunks indexados
avg_metadata_kb=1.5, # ~1.5 KB por metadata
queries_per_month=15_000_000, # 15M queries/mes
avg_read_units_per_query=2.0, # top_k=10 con filtro
upserts_per_month=200_000, # 200K nuevos chunks/mes
)
# {'storage_gb': 18.13, 'storage_cost': 5.98, 'read_cost': 247.50, 'write_cost': 0.40, 'total_monthly_usd': 253.88}
Compara contra ChromaDB self-hosted: instancia EC2 m6i.2xlarge ($200/mes) + EBS storage ($30/mes) + tu tiempo operacional. Cuando incluyes oncall, backups, escalado y monitoreo, los $250/mes de Pinecone empiezan a parecer baratos.
Calidad: que la migración no degrade resultados
Latencia y costo no sirven de nada si Pinecone devuelve peores documentos. Mide recall@k contra un ground truth:
def recall_at_k(retriever, queries_with_truth: list[tuple[str, set[str]]], k: int = 10) -> float:
hits = 0
total = 0
for query, expected_doc_ids in queries_with_truth:
results = retriever(query, top_k=k)
retrieved_ids = {r["id"] for r in results}
hits += len(expected_doc_ids & retrieved_ids)
total += len(expected_doc_ids)
return hits / total if total else 0.0
Esperado en una migración bien ejecutada (mismos chunks, mismos embeddings): recall_chroma ≈ recall_pinecone ± 1%. Si la diferencia es mayor, hay un bug en la migración (probablemente metadata mal copiado o IDs duplicados).
Tabla comparativa ejemplo
Resultado real de un benchmark sobre 5,000 queries en un dataset de 1.2M vectores:
| Métrica | ChromaDB local (m6i.2xlarge) | Pinecone serverless |
|---|---|---|
| p50 latencia | 145 ms | 78 ms |
| p95 latencia | 620 ms | 210 ms |
| p99 latencia | 1,420 ms | 340 ms |
| Throughput @50 conc | 65 QPS | 380 QPS |
| Recall@10 | 0.87 | 0.87 |
| Costo infra/mes | $230 | $254 |
| Operación | Oncall, backups manuales | Managed |
| Tiempo a recuperarse de falla | 30-60 min | <2 min |
Interpretación: Pinecone reduce p99 4×, soporta 6× más concurrencia, mantiene la misma calidad y cuesta marginalmente más en infraestructura pura. Cuando agregas costo operacional (un ingeniero dedicando 5 horas/semana a ChromaDB son ~$2,000/mes), Pinecone es claramente más barato.
Conexión con el proyecto Production RAG
Tu proyecto final del módulo debe incluir un informe BENCHMARK.md con:
- Setup: hardware, dataset size, configuración exacta
- Resultados de latencia: tabla con p50/p95/p99 para ChromaDB y Pinecone
- Resultados de throughput: gráfico QPS vs latencia a varios niveles de concurrencia
- Estimación de costo: a 6 meses con crecimiento proyectado
- Recall@10: validación que la calidad no degradó
- Recomendación: migrar/no migrar/migrar parcialmente, con razones
- Script reproducible: para correr cuando cambien condiciones
Este es el entregable que defiendes ante stakeholders no técnicos.
Troubleshooting
Problema 1: "Latencias inconsistentes entre runs"
Causa: condiciones de red variables, vecinos ruidosos en cloud, GC pauses.
Solución: corre el benchmark 3 veces a horas distintas y reporta la mediana de los runs. Excluye outliers extremos solo si tienes evidencia (logs de GC, métricas de CPU).
Problema 2: "Pinecone parece más lento en mi laptop"
Causa: latencia de red dominando. Tu laptop a us-west está a ~100ms del cluster Pinecone; ChromaDB local está a 0ms.
Solución: corre el benchmark desde la región donde correrá producción (EC2 en la misma región que el índice Pinecone). Sin esto, comparas red contra disco local.
Problema 3: "Costos reales explotaron vs estimación"
Causa: queries reales tienen top_k más alto que estimaste, o el filtro fuerza más read units.
Solución: revisa los logs de queries reales (no estimes, mide). Pinecone expone usage en el dashboard; correlaciona contra tus logs de aplicación.
Problema 4: "Throughput se cae después de 5 minutos"
Causa: rate limiting de Pinecone serverless en plan free/starter, o connection pool del cliente saturado.
Solución: verifica el plan; el plan starter tiene límites bajos. Para clientes Python, configura pool_threads apropiadamente y considera un cliente pool de keep-alive.
Problema 5: "Recall@k baja en Pinecone"
Causa típica: metadata se truncó o los IDs no son únicos durante la migración.
Solución: valida con index.fetch(ids=[...]) que el documento existe y tiene metadata correcto. Compara N documentos aleatorios entre ChromaDB y Pinecone byte a byte.
Ejercicios
Ejercicio 1: Calcular percentiles correctamente
Implementa una función que reciba latencias y retorne p50, p95, p99 sin usar bibliotecas externas. Cuidado con el cálculo del índice cuando el array es pequeño.
Ver solución
def percentile(values: list[float], p: float) -> float:
if not values:
return 0.0
sorted_vals = sorted(values)
k = (len(sorted_vals) - 1) * p
f = int(k)
c = min(f + 1, len(sorted_vals) - 1)
if f == c:
return sorted_vals[f]
return sorted_vals[f] * (c - k) + sorted_vals[c] * (k - f)
def all_percentiles(latencies: list[float]) -> dict:
return {
"p50": percentile(latencies, 0.50),
"p95": percentile(latencies, 0.95),
"p99": percentile(latencies, 0.99),
}
Explicación: la fórmula con interpolación lineal evita errores cuando len * 0.95 no es entero. min(f + 1, ...) previene IndexError cuando p=1.0.
Ejercicio 2: Benchmark con 1000 queries
Construye un benchmark completo que genere un reporte JSON con todas las métricas para ambos sistemas.
Ver solución
import json
def full_benchmark(chroma_query, pinecone_query, queries):
chroma_result = benchmark_retriever("chromadb", chroma_query, queries)
pinecone_result = benchmark_retriever("pinecone", pinecone_query, queries)
return {
"chromadb": chroma_result,
"pinecone": pinecone_result,
"improvement": {
"p95_speedup": round(chroma_result["p95"] / pinecone_result["p95"], 2),
"p99_speedup": round(chroma_result["p99"] / pinecone_result["p99"], 2),
},
}
report = full_benchmark(my_chroma_retriever, my_pinecone_retriever, test_queries[:1000])
with open("benchmark.json", "w") as f:
json.dump(report, f, indent=2)
Explicación: producir JSON facilita versionar resultados en git y comparar runs históricos.
Ejercicio 3: Estimación de costo a 12 meses con crecimiento
Asume crecimiento del 15% mensual en queries y vectores. Estima costo mensual al mes 12.
Ver solución
def project_12_month_cost(initial_vectors, initial_queries, monthly_growth_rate=0.15):
projections = []
vectors = initial_vectors
queries = initial_queries
for month in range(1, 13):
cost = estimate_pinecone_monthly(
vectors_total=vectors,
avg_metadata_kb=1.5,
queries_per_month=queries,
avg_read_units_per_query=2.0,
upserts_per_month=int(vectors * 0.05),
)
projections.append({"month": month, "vectors": vectors, "queries": queries, **cost})
vectors = int(vectors * (1 + monthly_growth_rate))
queries = int(queries * (1 + monthly_growth_rate))
return projections
projections = project_12_month_cost(1_000_000, 5_000_000)
print(f"Mes 1: ${projections[0]['total_monthly_usd']}")
print(f"Mes 12: ${projections[-1]['total_monthly_usd']}")
print(f"Total año: ${sum(p['total_monthly_usd'] for p in projections)}")
Explicación: el crecimiento compuesto sorprende. 15% mensual ≈ 5.4× al año. Si tu mes 1 cuesta $250, mes 12 puede costar $1,300+.
Ejercicio 4: Decisión automatizada con criterios explícitos
Crea una función que produzca recomendación textual basada en thresholds claros.
Ver solución
def migration_recommendation(benchmark_report: dict, monthly_cost: float, ops_cost: float = 2000) -> dict:
chroma = benchmark_report["chromadb"]
pinecone = benchmark_report["pinecone"]
p95_speedup = chroma["p95"] / pinecone["p95"]
p99_speedup = chroma["p99"] / pinecone["p99"]
total_pinecone = monthly_cost
total_chroma = monthly_cost * 0.7 + ops_cost # infra + ops time
reasons = []
if p99_speedup >= 2:
reasons.append(f"p99 mejora {p99_speedup:.1f}×")
if total_pinecone < total_chroma:
reasons.append(f"Costo total ${total_pinecone:.0f}/mes vs ${total_chroma:.0f}/mes")
if not reasons:
return {"decision": "No migrar", "reasons": ["Sin ventaja clara"]}
return {
"decision": "Migrar",
"reasons": reasons,
"monthly_savings": round(total_chroma - total_pinecone, 2),
}
Explicación: explicitar thresholds (≥2× speedup, costo total incluyendo ops) hace la recomendación defendible. Cuando el CFO pregunte "¿por qué?", apuntas al código.
Ejercicio 5: Generar gráfico de QPS vs latencia
Usa matplotlib para visualizar throughput vs latencia a múltiples niveles de concurrencia.
Ver solución
import matplotlib.pyplot as plt
async def concurrency_curve(async_query, queries, levels=[1, 10, 25, 50, 100, 200]):
results = []
for c in levels:
r = await measure_throughput(async_query, queries, concurrency=c, duration_sec=120)
results.append(r)
return results
def plot_curve(chroma_results, pinecone_results):
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot([r["qps"] for r in chroma_results], [r["p95_ms"] for r in chroma_results],
"o-", label="ChromaDB")
ax.plot([r["qps"] for r in pinecone_results], [r["p95_ms"] for r in pinecone_results],
"s-", label="Pinecone")
ax.set_xlabel("Throughput (QPS)")
ax.set_ylabel("p95 latencia (ms)")
ax.set_title("Latencia vs Throughput bajo concurrencia")
ax.legend()
ax.grid(True, alpha=0.3)
plt.savefig("throughput_vs_latency.png", dpi=150)
Explicación: la curva muestra dónde cada sistema empieza a degradarse. ChromaDB típicamente sube vertical antes; Pinecone se mantiene plano más tiempo. Ese plano es tu margen operacional.
Resumen
- Benchmarking convierte intuiciones en decisiones defendibles con datos
- Mide p95 y p99, no solo promedio: la cola define la experiencia del usuario
- Throughput bajo concurrencia importa más que latencia secuencial para producción
- Costo total incluye infraestructura + tiempo operacional, no solo factura cloud
- Pinecone serverless cobra por storage + read units + write units; estima con la calculadora oficial
- Valida recall@k para asegurar que la migración no degrade calidad
- El entregable final es un
BENCHMARK.mdreproducible con script versionado
Recursos adicionales
- Pinecone Pricing - Precios oficiales serverless y pod-based.
- Pinecone Performance Monitoring - Métricas operativas en dashboard.
- Measuring Latency Percentiles - Por qué el promedio es engañoso.
- Locust - Framework profesional para load testing.
- Pinecone Read Unit Calculator - Cómo se computan read units.
Creado: Marzo 13, 2026
Versión: 2.0