惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
H
Hacker News: Front Page
Stack Overflow Blog
Stack Overflow Blog
B
Blog
I
InfoQ
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
月光博客
月光博客
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
F
Full Disclosure
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recent Announcements
Recent Announcements
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Jina AI
Jina AI
T
Tailwind CSS Blog
S
Secure Thoughts
P
Privacy International News Feed
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 最新话题
H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
Cybersecurity and Infrastructure Security Agency CISA
Last Week in AI
Last Week in AI
W
WeLiveSecurity
Google Online Security Blog
Google Online Security Blog
P
Privacy & Cybersecurity Law Blog
D
DataBreaches.Net
Engineering at Meta
Engineering at Meta
Know Your Adversary
Know Your Adversary
P
Palo Alto Networks Blog
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Project Zero
Project Zero
V2EX - 技术
V2EX - 技术
H
Heimdal Security Blog
博客园 - Franky
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
H
Help Net Security
Martin Fowler
Martin Fowler
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
G
GRAHAM CLULEY
博客园 - 【当耐特】

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
HikariCP: el p95 que te miente y cómo leer las señales reales del pool
Juan Torchia · 2026-05-15 · via DEV Community

HikariCP: el p95 que te miente y cómo leer las señales reales del pool

Hubo una versión de este análisis que empezaba mal. Miraba el p95 del escenario tiny con delay de 500ms y veía 260.78ms. Comparado con el escenario default que mostraba 2418.16ms, parecía casi cinco veces más rápido. Eso es una trampa clásica, y casi me la como.

El escenario tiny tenía 97.05% de error rate. De 8139 intentos, 7899 fallaban. Los 260ms eran el tiempo promedio de rechazo, no de respuesta útil. No era rápido — estaba fallando rápido. Y la diferencia importa muchísimo cuando estás intentando entender si la configuración de HikariCP sirve o no.

Eso me llevó a armar hikaricp-pool-experiment: un laboratorio reproducible con Java 21, Spring Boot 3.4.5, PostgreSQL 16, HikariCP, Docker Compose y k6 0.51.0. El objetivo no fue simular producción ni documentar un incidente real. Fue construir un entorno donde las señales del pool fueran visibles y medibles, para poder razonar sobre ellas con números en la mano.


El diseño del experimento

La app expone dos endpoints:

  • GET /api/query?delayMs=500: ejecuta una consulta real contra PostgreSQL y retiene la conexión usando pg_sleep durante el tiempo indicado.
  • GET /api/pool: devuelve el estado del pool en tiempo real — active, idle, total, threadsAwaitingConnection y la configuración efectiva.

El delayMs es el mecanismo central del experimento. Una query instantánea puede esconder contención aunque la concurrencia sea alta porque las conexiones se liberan antes de que el siguiente request las necesite. Con pg_sleep(0.5), cada conexión queda ocupada durante medio segundo. Con 50 usuarios virtuales golpeando en paralelo, la presión sobre el pool se vuelve visible rápido.

El script de k6 hace algo que el draft original no tenía bien separado: registra query_duration para todos los intentos y query_success_duration solo para los que devuelven HTTP 200. Sin esa distinción, el p95 agrega rechazos rápidos con queries exitosas lentas y el número resultante no representa ninguna realidad útil.

// load/hikari-pool.js — separación crítica entre intentos totales y exitosos
const ok = check(queryResponse, {
  'query status is 200': (response) => response.status === 200,
});
queryDuration.add(queryResponse.timings.duration);
if (ok) {
  querySuccessDuration.add(queryResponse.timings.duration);
}
queryErrors.add(!ok);

Enter fullscreen mode Exit fullscreen mode

Los escenarios definidos en application.yml son:

Escenario maximumPoolSize connectionTimeout
default 10 (Spring Boot default) 30000ms (HikariCP default)
tiny 2 250ms
pool4 4 1500ms
pool8 8 1500ms
pool16 16 1500ms
pool32 32 1500ms

La matriz se corrió con dos delays — 50ms y 500ms — porque el contraste es importante: una query que libera la conexión rápido y una query que la retiene durante medio segundo no estresan el pool de la misma manera.

Para reproducirlo desde cero:

.\scripts\run-matrix.ps1 -Vus 50 -Duration 60s

Enter fullscreen mode Exit fullscreen mode

O escenario por escenario:

docker compose down -v
.\scripts\run-scenario.ps1 -Scenario tiny -Vus 50 -Duration 60s -DelayMs 500
.\scripts\run-scenario.ps1 -Scenario pool16 -Vus 50 -Duration 60s -DelayMs 500

Enter fullscreen mode Exit fullscreen mode

Limitación importante: todo esto es un single local run del 2026-05-14 en Windows con Docker Desktop/WSL2. Los números sirven para comparar escenarios dentro de la misma máquina. No son un benchmark universal ni reflejan comportamiento en ningún entorno en la nube, Railway o de otro tipo. pg_sleep retiene conexiones de forma artificial para hacer visible la presión — no representa una workload real de producción.


Los resultados completos — y qué leer en ellos

Esta es la tabla que generó summarize-results.ps1 a partir de los JSON de k6:

Escenario Delay Intentos Exitosas Fallidas Error rate Exitosas/s p95 todos p95 exitosas Active máx. Waiting máx.
default 50ms 11772 11772 0 0% 195.38 165.7ms 165.7ms 10 30
default 500ms 1240 1240 0 0% 19.85 2418.16ms 2418.16ms 10 39
tiny 50ms 8289 2325 5964 71.95% 38.53 298.81ms 304.84ms 2 47
tiny 500ms 8139 240 7899 97.05% 3.97 260.78ms 752.51ms 2 47
pool4 50ms 4712 4712 0 0% 77.75 557.55ms 557.55ms 4 43
pool4 500ms 1779 492 1287 72.34% 7.95 1962.83ms 1990.52ms 4 45
pool8 50ms 9253 9253 0 0% 153.4 365.15ms 365.15ms 8 41
pool8 500ms 1653 984 669 40.47% 15.87 1996.36ms 1998.83ms 8 41
pool16 50ms 18155 18155 0 0% 301.83 82.92ms 82.92ms 16 40
pool16 500ms 1948 1947 1 0.05% 31.62 1492.44ms 1492.44ms 16 31
pool32 50ms 18892 18892 0 0% 314.16 70.33ms 70.33ms 32 32
pool32 500ms 3830 3830 0 0% 63.00 784.9ms 784.9ms 32 24

Hay varias cosas que vale la pena leer juntas, no por separado.


La trampa del p95 bajo con error rate alto

El caso tiny con delay 500ms es el más instructivo del experimento. El p95 de todos los intentos es 260.78ms. Si solo mirás ese número, parecería que el pool responde muy rápido. Pero el 97.05% de error rate te dice que casi ninguna query llegó a ejecutarse — HikariCP estaba rechazando requests en connectionTimeout: 250ms porque no había conexiones libres.

La separación entre query_duration y query_success_duration hace visible lo que el número agregado escondía: el p95 de las queries exitosas es 752.51ms — casi tres veces más. Esas pocas queries que sí consiguieron una conexión tardaron casi un segundo, probablemente porque esperaron a que alguna de las dos conexiones del pool se liberara.

Cuando active está pegado al máximo del pool (2/2) y waiting llega a 47, el sistema no está procesando carga — la está rechazando. Los 260ms son el tiempo de fracaso, no de éxito.

Señal que importa: si p95 todos los intentosp95 exitosas y el error rate es alto, el pool está en exhaustion. No estás viendo latencia de queries: estás viendo latencia de rechazo.


Cómo leer las cuatro señales en conjunto

El experimento confirmó que ninguna métrica sola alcanza. Las señales que tiene sentido cruzar son:

1. Error rate + successful queries/s

Estas dos juntas son el primer filtro. Un error rate de 0% con 19.85 exitosas/s (default, delay 500ms) es muy diferente a un error rate de 97% con 3.97 exitosas/s (tiny, delay 500ms). El throughput de exitosas dice cuánto trabajo útil hace el sistema; el error rate dice cuánto trabajo está tirando a la basura.

En pool4 con delay 500ms: 72.34% de error rate con solo 7.95 exitosas/s. Cuatro conexiones con queries de 500ms dan un techo teórico de 8 exitosas/s (4 conexiones × 2 por segundo). Los números coinciden: el pool está al límite y rechaza el resto.

2. active = maximumPoolSize sostenido + waiting > 0

Esta combinación es la señal operativa más directa de que el pool está bajo presión. Cuando maxActiveConnections bate el techo configurado y maxThreadsAwaitingConnection es mayor que cero durante un período sostenido, los threads de la aplicación están esperando una conexión que no está disponible.

Del experimento:

  • tiny delay 500ms: active máx. 2/2, waiting máx. 47. Pool exhausto desde el principio.
  • pool8 delay 500ms: active máx. 8/8, waiting máx. 41, error rate 40.47%. Presión alta pero no total.
  • pool32 delay 500ms: active máx. 32/32, waiting máx. 24, error rate 0%. El pool llega al techo pero absorbe la carga sin rechazar.

En pool32 con delay 500ms, waiting = 24 con error rate 0% significa que los threads esperan pero el connectionTimeout: 1500ms alcanza — las queries encolan y eventualmente consiguen conexión. Es un sistema bajo presión que aún funciona, no uno en crisis.

3. Latencia de intentos vs. latencia de exitosas

Ya mencioné el caso tiny. Pero vale generalizar: cuando hay error rate significativo, el p95 de todos los intentos deja de ser una métrica de performance de la aplicación y pasa a ser una métrica de velocidad de rechazo. La latencia operativa real es la de las queries exitosas.

En pool4 delay 500ms: p95 todos los intentos 1962.83ms, p95 exitosas 1990.52ms. Acá los números son similares porque las queries que sí pasan también esperan mucho — el pool tiene 4 conexiones con queries de 500ms, así que casi todo el tiempo está esperando que alguna se libere.

4. El salto de 50ms a 500ms como revelador de presión

Con delay 50ms, pool8 no tiene un solo error y procesa 153.4 exitosas/s. Con delay 500ms, cae a 40.47% de error rate y 15.87 exitosas/s. El pool no cambió — cambió el tiempo de retención de la conexión. Si cada conexión tarda diez veces más en liberarse, el pool que antes era suficiente ahora no alcanza.

Esta es la variable que más frecuentemente se ignora cuando se calibra un pool: no es solo cuántas conexiones hay, sino cuánto tiempo cada query las retiene. Un pool de 16 conexiones con queries de 50ms es muy diferente a un pool de 16 conexiones con queries de 500ms.


El límite del salto de pool16 a pool32 con delay corto

Hay una observación del experimento que me parece importante para evitar la conclusión fácil de "más conexiones = mejor".

Con delay 50ms:

  • pool16: 301.83 exitosas/s, p95 82.92ms
  • pool32: 314.16 exitosas/s, p95 70.33ms

Doblar el tamaño del pool dio una mejora de apenas ~4% en throughput. El salto de pool8 a pool16 fue mucho mayor (153.4 → 301.83, casi el doble). A partir de cierto punto, el cuello de botella deja de ser el pool y pasa a ser otra cosa — en este caso, probablemente el CPU del Docker Desktop o el propio PostgreSQL bajo carga de 50 VUs.

Esto es consistente con la fórmula que Brettwooldridge menciona en el README de HikariCP: el pool óptimo para throughput de base de datos no es simplemente "el más grande posible". Más allá de cierto umbral, agregar conexiones genera overhead sin beneficio real, y en un entorno con límites de max_connections en PostgreSQL podés quedarte sin slots antes de que el throughput mejore.

La conclusión práctica del experimento no es que 32 sea el número correcto. Es que pool16 con delay 500ms tiene 0.05% de error rate y pool32 tiene 0%, con un throughput 2x mayor. Dependiendo de los tiempos reales de las queries y los límites de la PostgreSQL, el trade-off es diferente en cada caso.


Las métricas que expone el experimento vía Actuator

La app tiene Actuator habilitado con health, info, metrics y prometheus. Durante una corrida podés consultar el estado del pool directamente:

# Estado del pool vía endpoint propio
curl http://localhost:8080/api/pool

# Métricas Micrometer vía Actuator
curl http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl http://localhost:8080/actuator/metrics/hikaricp.connections.pending
curl http://localhost:8080/actuator/metrics/hikaricp.connections.timeout

Enter fullscreen mode Exit fullscreen mode

El endpoint /api/pool usa HikariPoolMXBean directamente y devuelve active, idle, total, threadsAwaitingConnection y la configuración efectiva. Es lo que k6 consulta en paralelo para registrar las métricas hikari_pool_active, hikari_pool_idle, hikari_pool_total y hikari_pool_threads_awaiting_connection.

La métrica hikaricp.connections.timeout de Actuator es la que más me interesa en cualquier entorno real: cuenta las veces que un thread esperó una conexión y se venció el connectionTimeout. Si ese contador es mayor que cero, hay usuarios afectados — no es una advertencia, es un hecho.


La configuración del experimento vs. configuración para un entorno real

El experimento usa valores diseñados para hacer visible la presión en un laboratorio, no valores para copiar en cualquier sistema. El perfil tiny tiene connectionTimeout: 250ms porque 250ms hace que el pool rechace requests rápido y los errores sean inmediatamente visibles. En un sistema real, 250ms es probablemente demasiado agresivo — vas a generar falsos positivos ante cualquier pico breve.

Lo que sí traslada son los principios de lectura:

Sobre connectionTimeout: el valor define la velocidad del fallo, no la velocidad del éxito. Un timeout corto genera errores más rápido y hace que los síntomas sean visibles antes. Un timeout largo acumula threads bloqueados que consumen memoria y pueden saturar el thread pool del servidor web antes de que el error sea obvio. Cuál de los dos querés depende de si tenés circuit breakers y retry logic, y de cuánto tiempo puede esperar un usuario antes de que la experiencia se rompa.

Sobre maximumPoolSize: el número correcto depende del tiempo promedio de retención de las queries, la concurrencia esperada, y los límites de max_connections de la PostgreSQL. No hay una fórmula universal. Lo que el experimento muestra es que con queries de 500ms y 50 VUs, necesitás al menos 16 conexiones para llegar a error rate cercano a cero — y que doblar a 32 da rendimientos marginales decrecientes en el throughput.

Sobre bases de datos gestionadas en la nube: si usás Railway, Supabase, RDS u otro servicio donde no controlás el servidor directamente, hay un parámetro adicional que importa y que el experimento no cubre: maxLifetime. El servidor puede cerrar conexiones inactivas antes del default de 30 minutos de HikariCP, y una conexión que desde el pool "está viva" pero el servidor ya cerró va a generar PSQLException: This connection has been closed en el próximo uso. Configurar maxLifetime por debajo del timeout del servidor es un ajuste necesario en esos entornos — pero no es algo que este laboratorio local con Docker pueda medir.


Mi postura después del experimento

Lo más valioso del ejercicio no fue elegir un número de conexiones. Fue entender que HikariCP no se ajusta mirando una sola métrica.

Si solo mirás el p95 de todos los intentos, podés concluir que un pool en crisis es "rápido". Si solo mirás el error rate, no sabés si el sistema está absorbiendo carga o rechazándola. Si solo mirás active, no sabés si el pool tiene margen o está al límite. Necesitás cruzar los cuatro: error rate, successful queries/s, active vs. máximo configurado, waiting, y latencia de exitosas.

El otro aprendizaje que me quedó: hay dos formas de que un pool falle bajo carga. Una es el timeout largo — threads que esperan 30 segundos y eventualmente explotan el heap. La otra es el timeout corto — rechazos rápidos que generan error rate alto pero dan la ilusión de baja latencia. El laboratorio hizo visible las dos con números reales.

No compro la idea de que hay un maximumPoolSize correcto universal. Lo que hay es un tamaño correcto para la combinación de tiempo de query, concurrencia esperada, y capacidad de la base de datos. Y ese número solo tiene sentido leído junto con el tiempo de retención de conexiones y la tasa de error — no en aislamiento.

El repo tiene todo lo necesario para correrlo de nuevo en la entorno y comparar:

.\scripts\run-matrix.ps1 -Vus 50 -Duration 60s

Enter fullscreen mode Exit fullscreen mode

Si cambiás el delay, la concurrencia o el maximumPoolSize, las señales cambian. Eso es exactamente el punto.

github.com/JuanTorchia/hikaricp-pool-experiment


Referencia: