# DOC-OPS-008 · Stress Test Python — face-match-node

**Producto:** Microservicio face-match-node · Fidelitas S.A.  
**Versión:** 1.0  
**Fecha:** 2026-11-10  
**Audiencia:** Developer Junior / DevOps  
**Clasificación:** CONFIDENCIAL — USO INTERNO

---

## Índice

1. Para qué sirve este script y qué mide
2. Prerequisitos
3. Uso básico — test local
4. Uso avanzado — test contra el cloud
5. Parámetros disponibles
6. Qué registra en la base de datos
7. Cómo leer los resultados en pantalla
8. Cómo consultar los KPIs desde SQLite
9. Cómo leer los resultados del servidor mientras corre el test
10. Interpretar los resultados — qué es bueno y qué no
11. Ejemplo de salida completa anotada

---

## 1. Para qué sirve este script y qué mide

`stress_test.py` envía **N requests por segundo durante T segundos** contra el endpoint `POST /v3/face-match/` y produce:

1. **Métricas de throughput** — cuántos RPS reales alcanzó el servidor.
2. **Métricas de latencia** — distribución en percentiles (p50, p90, p95, p99, máximo).
3. **Distribución de errores HTTP** — qué códigos devolvió el servidor y en qué proporción.
4. **Registro persistente** — cada transacción se guarda en la tabla `load_test_results` de la misma DB SQLite del servicio.
5. **KPI interpretado** — al final clasifica la latencia y el throughput automáticamente.

### Qué se mide realmente sin credenciales RENAPER

Cuando se corre en desarrollo o sin credenciales RENAPER reales, el servidor procesa cada request hasta el punto en que intenta conectar con RENAPER y falla (HTTP 500 o 502). **Eso es suficiente para medir la infraestructura:**

| Qué se mide | Qué NO se mide |
|---|---|
| Latencia de Fastify (autenticación, validación, routing) | RTT hacia RENAPER |
| Capacidad del rate limiter | Tiempo de procesamiento RENAPER |
| Throughput real del servidor Node.js | Score de face-match |
| Estabilidad bajo carga sostenida (crashes, memory leaks) | |
| Comportamiento de SQLite bajo carga de escritura | |

Cuando hay credenciales RENAPER reales, el test mide el circuito completo end-to-end incluyendo RTT a RENAPER y tiempo de procesamiento biométrico.

---

## 2. Prerequisitos

### Python y librería

```bash
# Verificar versión de Python (se requiere 3.10+)
python3 --version

# Instalar aiohttp (cliente HTTP async) si no está instalado
pip3 install aiohttp

# En sistemas con pip restringido (Ubuntu 24.04):
pip3 install aiohttp --break-system-packages
```

### El servidor face-match-node debe estar corriendo

```bash
# Verificar que el servidor responde antes de lanzar el test
curl -s http://localhost:3100/health
# {"status":"ok","service":"fide-facematch-api","uptime":X}

# Si no responde, levantarlo:
node src/server.js &        # desarrollo, proceso background
# o en producción:
pm2 start ecosystem.config.js --env production
```

### El archivo .env debe existir

El script lee automáticamente el `.env` del proyecto para obtener:
- `APIKEY_MASTER` — la API key del servicio
- `DB_PATH` — la ruta de la base de datos SQLite

Si el `.env` no existe o no tiene esas variables, el script usa defaults seguros:
- API key: `fide-dev-local-test-key-2026`
- DB: `./data/fide-facematch.db`

---

## 3. Uso básico — test local

```bash
# Desde la carpeta raíz del proyecto
cd /home/memyself/Downloads/fide-workspace/face-match-node

# Test estándar: 200 RPS durante 60 segundos (12.000 requests total)
python3 stress_test.py

# Test rápido para verificar que todo funciona: 10 RPS por 5 segundos
python3 stress_test.py --rps 10 --duration 5
```

El script lee automáticamente la API key y la ruta de la DB desde el `.env` del proyecto. No hace falta configurar nada más para el test local.

---

## 4. Uso avanzado — test contra el cloud

### 4.1 Test básico contra producción

```bash
python3 stress_test.py \
  --url https://apibatch.fidelitas.com.ar \
  --apikey TU_APIKEY_PRODUCCION \
  --rps 50 \
  --duration 30
```

> **Atención:** un test de 200 RPS contra producción consume quota de RENAPER si las credenciales son reales. Coordinar con el equipo antes de lanzar tests de alta carga en producción.

### 4.2 Test contra cloud sin consumir RENAPER

Usar un DNI que se sabe que no existe en RENAPER o coordinar una ventana de mantenimiento. Los requests llegarán al servidor, serán autenticados y procesados, y solo fallarán en el paso de RENAPER.

```bash
python3 stress_test.py \
  --url https://apibatch.fidelitas.com.ar \
  --apikey TU_APIKEY_PRODUCCION \
  --rps 100 \
  --duration 60 \
  --db ./data/fide-facematch.db
```

> Si el servidor cloud tiene la DB en `/var/www/html/apibatch/data/fide-facematch.db`, los resultados del test se guardan en la DB **local** (donde corre el script Python), no en el servidor. Para guardar en el servidor, copiar el script al servidor y correrlo desde ahí.

### 4.3 Test corriendo el script desde el servidor cloud

```bash
# Copiar el script al servidor
scp stress_test.py ubuntu@SERVER_IP:/var/www/html/apibatch/

# Instalar aiohttp en el servidor
ssh ubuntu@SERVER_IP
pip3 install aiohttp --break-system-packages

# Correr el test desde el servidor (guarda en la DB de producción)
cd /var/www/html/apibatch
python3 stress_test.py \
  --url http://localhost:3100 \
  --rps 200 \
  --duration 60 \
  --db /var/www/html/apibatch/data/fide-facematch.db
```

Cuando el script corre en el mismo servidor, el tráfico no pasa por Internet ni por Apache — mide el Node.js puro. Para medir el stack completo (Apache + TLS + Node.js), correrlo desde otra máquina con la URL HTTPS.

---

## 5. Parámetros disponibles

| Parámetro | Default | Descripción |
|---|---|---|
| `--rps` | `200` | Requests por segundo objetivo |
| `--duration` | `60` | Duración del test en segundos |
| `--url` | `http://localhost:3100` | URL base del servidor (sin path) |
| `--apikey` | (lee del .env) | API key — si no se pasa, lee `APIKEY_MASTER` del `.env` |
| `--nationalid` | `22222222` | DNI usado en todos los requests de prueba |
| `--gender` | `M` | Género del DNI de prueba (`M` o `F`) |
| `--db` | (lee del .env) | Ruta a la DB SQLite donde se guardan los resultados |
| `--concurrency` | `400` | Máximo de requests en vuelo simultáneamente |

### Combinaciones frecuentes

```bash
# Test rápido de verificación (10s)
python3 stress_test.py --rps 50 --duration 10

# Test de saturación (comprobar qué pasa a 500 RPS)
python3 stress_test.py --rps 500 --duration 30

# Test contra cloud en HTTPS (mide stack completo)
python3 stress_test.py \
  --rps 100 --duration 60 \
  --url https://apibatch.fidelitas.com.ar \
  --apikey MI_APIKEY

# Test largo de estabilidad (mide si hay memory leaks o degradación)
python3 stress_test.py --rps 50 --duration 600
```

---

## 6. Qué registra en la base de datos

El script crea automáticamente la tabla `load_test_results` en la DB SQLite si no existe, y registra **un fila por cada request enviado**:

```sql
CREATE TABLE load_test_results (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    test_run_id     TEXT NOT NULL,      -- identificador único del test run (ej: lt_1794343790)
    seq             INTEGER NOT NULL,   -- número de orden del request (1, 2, 3, ...)
    sent_at         REAL NOT NULL,      -- timestamp Unix con microsegundos (epoch float)
    response_ms     INTEGER,            -- latencia total en milisegundos (NULL si timeout)
    http_status     INTEGER,            -- código HTTP: 200, 400, 401, 429, 500, 502, etc.
    api_status      TEXT,               -- campo "Status" del JSON de respuesta: "OK" o "ERROR"
    api_message     TEXT,               -- campo "Message" del JSON de respuesta
    error           TEXT,               -- descripción del error de red (NULL si llegó al servidor)
    created_at      TEXT DEFAULT (datetime('now','localtime'))
);
```

Cada ejecución del script tiene un `test_run_id` único (`lt_` + timestamp Unix), lo que permite distinguir múltiples runs en la misma DB.

---

## 7. Cómo leer los resultados en pantalla

Al terminar el test, el script imprime un resumen estructurado. Este es un ejemplo con los resultados de un test local sin credenciales RENAPER:

```
=================================================================
  STRESS TEST — RESUMEN DE KPIs
  Run ID   : lt_1794343790
  Timestamp: 2026-11-10 17:50:51
=================================================================

  CONFIGURACIÓN
    RPS objetivo      : 200
    Duración objetivo  : 60 s
    Duración real      : 60.02 s            ← el test corrió exactamente 60s

  THROUGHPUT
    Requests enviados  :   12,001
    RPS real           :   199.95           ← el rate limiter fue preciso al 99.97%
    Llegaron al server :   12,001  (100.0%) ← 0 errores de red
    HTTP 200           :        0  (0.0%)   ← esperado sin RENAPER real

  LATENCIA (ms) — sobre 12,001 respuestas con tiempo medido
    Mínimo             :        1 ms
    Mediana (p50)      :        2 ms        ← la mitad de los requests en 2ms
    Promedio           :       21 ms
    p90                :       45 ms        ← 90% en menos de 45ms
    p95                :      112 ms
    p99                :      532 ms        ← el 1% más lento tomó 532ms
    Máximo             :      756 ms
    Desv. estándar     :       76 ms

  DISTRIBUCIÓN HTTP STATUS
       500  : 10,802  ( 90.0%)  ████...     ← falla RENAPER (esperado sin credenciales)
       502  :  1,199  ( 10.0%)  ████        ← falla RENAPER upstream

  DISTRIBUCIÓN API STATUS (campo 'Status' en body JSON)
         ERROR  : 12,001  (100.0%)          ← todos devuelven JSON válido con Status=ERROR

  ERRORES
    Errores de red     :      0  (0.0%)     ← el servidor nunca rechazó conexiones
    Timeouts           :      0

  KPIs CLAVE
    Latencia p99       : BUENO (<1s p99)
    Throughput         : 200.0 rps reales / 200 objetivo
    Alcance            : 100.0% de requests llegaron al servidor
```

**Qué significa cada sección:**

| Sección | Qué indica |
|---|---|
| **THROUGHPUT** | Cuántos requests por segundo procesó el servidor realmente |
| **LATENCIA** | Distribución de tiempos de respuesta — los percentiles son más importantes que el promedio |
| **DISTRIBUCIÓN HTTP STATUS** | Qué devolvió el servidor — 200 es éxito, 429 es rate limit, 500/502 es falla de backend |
| **DISTRIBUCIÓN API STATUS** | Campo "Status" del JSON — "OK" o "ERROR" independientemente del HTTP status |
| **ERRORES** | Fallos de red — el servidor no respondió, timeout, conexión rechazada |
| **KPIs CLAVE** | Interpretación automática de los resultados |

---

## 8. Cómo consultar los KPIs desde SQLite

Los datos persisten en la DB SQLite del servicio. Se pueden consultar con `sqlite3` desde el servidor o con cualquier cliente SQLite.

### Resumen del último test run

```sql
-- Sustituir lt_1794343790 con el Run ID que imprimió el script
SELECT
  COUNT(*)                                        AS total_requests,
  ROUND(AVG(response_ms), 0)                     AS avg_ms,
  MIN(response_ms)                               AS min_ms,
  MAX(response_ms)                               AS max_ms,
  SUM(CASE WHEN http_status = 200  THEN 1 END)  AS http_200,
  SUM(CASE WHEN http_status = 429  THEN 1 END)  AS http_429,
  SUM(CASE WHEN http_status = 500  THEN 1 END)  AS http_500,
  SUM(CASE WHEN http_status = 502  THEN 1 END)  AS http_502,
  SUM(CASE WHEN error IS NOT NULL  THEN 1 END)  AS errores_red
FROM load_test_results
WHERE test_run_id = 'lt_1794343790';
```

### Percentiles del último test run

SQLite no tiene función nativa de percentil, pero se puede calcular con ranking:

```sql
-- p50, p90, p99 del test lt_1794343790
SELECT
  percentile AS pct,
  response_ms
FROM (
  SELECT
    response_ms,
    CAST(ROW_NUMBER() OVER (ORDER BY response_ms) * 100.0 / COUNT(*) OVER () AS INTEGER) AS percentile
  FROM load_test_results
  WHERE test_run_id = 'lt_1794343790'
    AND response_ms IS NOT NULL
)
WHERE percentile IN (50, 90, 95, 99)
GROUP BY percentile
ORDER BY percentile;
```

### Comparar múltiples test runs

```sql
SELECT
  test_run_id,
  COUNT(*)                        AS total,
  ROUND(AVG(response_ms), 0)     AS avg_ms,
  SUM(CASE WHEN http_status < 300 THEN 1 ELSE 0 END) AS ok,
  MIN(created_at)                AS inicio
FROM load_test_results
GROUP BY test_run_id
ORDER BY MIN(created_at) DESC
LIMIT 10;
```

### Ver distribución por segundo (evolución de la latencia durante el test)

```sql
-- Latencia promedio por segundo de test — detecta degradación gradual
SELECT
  CAST((sent_at - MIN(sent_at) OVER()) AS INTEGER) AS segundo,
  COUNT(*)                                          AS requests,
  ROUND(AVG(response_ms), 0)                       AS avg_ms,
  MAX(response_ms)                                 AS max_ms
FROM load_test_results
WHERE test_run_id = 'lt_1794343790'
GROUP BY CAST((sent_at - MIN(sent_at) OVER()) AS INTEGER)
ORDER BY segundo;
```

### Comandos de consola rápidos

```bash
# Listar todos los test runs en la DB
sqlite3 data/fide-facematch.db \
  "SELECT test_run_id, COUNT(*) as n, MIN(created_at) as inicio
   FROM load_test_results GROUP BY test_run_id ORDER BY inicio DESC LIMIT 10;"

# Resumen del test run más reciente
sqlite3 data/fide-facematch.db \
  "SELECT test_run_id, COUNT(*), ROUND(AVG(response_ms),0), MAX(response_ms)
   FROM load_test_results
   WHERE test_run_id = (SELECT test_run_id FROM load_test_results ORDER BY id DESC LIMIT 1)
   GROUP BY test_run_id;"

# Distribución de HTTP status del run más reciente
sqlite3 data/fide-facematch.db \
  "SELECT http_status, COUNT(*) FROM load_test_results
   WHERE test_run_id = (SELECT test_run_id FROM load_test_results ORDER BY id DESC LIMIT 1)
   GROUP BY http_status ORDER BY http_status;"
```

---

## 9. Cómo leer los resultados del servidor mientras corre el test

Mientras `stress_test.py` corre, se puede monitorear el servidor en tiempo real desde otra terminal.

### Métricas en vivo desde el endpoint /stats/live

```bash
# Polling cada 2 segundos durante el test
watch -n2 "curl -s http://localhost:3100/stats/live | python3 -m json.tool"
```

Respuesta de ejemplo durante el test:

```json
{
    "active_now": 143,
    "rps_last_min": "198.50",
    "uptime_s": 320
}
```

### Dashboard web durante el test

Abrir en el browser (si la IP está en `DASHBOARD_ALLOWED_IPS`):

```
http://localhost:3100/dashboard
```

El dashboard actualiza automáticamente cada 2 segundos y muestra:
- RPS en tiempo real
- Latencia promedio
- Requests activos
- Histograma de distribución por hora

### Logs de PM2 en tiempo real

```bash
pm2 logs fide-facematch
```

Durante el test verás una línea por request con el evento, TCN, status y timings.

### Recursos del sistema durante el test

```bash
# CPU y memoria de cada worker PM2
pm2 monit

# CPU, memoria y red del sistema completo
htop

# Conexiones TCP activas al servicio
watch -n1 "ss -tnp | grep :3100 | wc -l"
```

### Consultar la DB mientras el test está corriendo

```bash
# Contar requests registrados hasta ahora (se actualiza en tiempo real)
watch -n2 "sqlite3 data/fide-facematch.db \
  \"SELECT COUNT(*) as registros FROM load_test_results
    WHERE test_run_id = (SELECT test_run_id FROM load_test_results ORDER BY id DESC LIMIT 1);\""
```

---

## 10. Interpretar los resultados — qué es bueno y qué no

### Latencia — valores de referencia

| Resultado | Qué significa | Acción |
|---|---|---|
| p99 < 500ms | EXCELENTE — el stack maneja la carga sin problemas | Nada |
| p99 < 1s | BUENO — aceptable para operaciones con backend externo | Monitorear |
| p99 < 3s | ACEPTABLE — límite del timeout de RENAPER (5 retries × 500ms) | Investigar causas |
| p99 > 3s | ALTO — requests están esperando recursos o hay contención | Reducir carga o revisar |
| Máximo > 10s | CRÍTICO — requests están timeouteando | Revisar RENAPER o ajustar pool |

### Throughput

| Resultado | Qué significa |
|---|---|
| RPS real ≈ RPS objetivo (±5%) | El servidor tiene capacidad de sobra |
| RPS real < 90% del objetivo | El servidor está saturado — hay contención |
| RPS real = 0 después de N segundos | El servidor se cayó bajo carga |

### Distribución de HTTP status

| Código | Qué indica |
|---|---|
| `200` | Éxito completo (incluye llamada exitosa a RENAPER) |
| `400` | Payload inválido — problema en el cliente |
| `401` | API key inválida o IP no autorizada |
| `429` | Rate limit activado — subir `RATE_LIMIT_PER_IP_PER_MIN` en `.env` o reducir RPS |
| `500` | Error interno (generalmente: falla de RENAPER o DB) |
| `502` | RENAPER no respondió o respondió mal |

### Errores de red

| Resultado | Qué indica |
|---|---|
| Errores de red = 0 | El servidor nunca rechazó conexiones ni timeouteó |
| Errores de red > 1% | El servidor está sobrecargado o cayó |
| Timeouts > 0 | Requests tomaron más de 35 segundos — crítico |

### Resultado esperado sin credenciales RENAPER

Este es el resultado esperado cuando el servidor corre sin credenciales RENAPER reales (el caso del test de infraestructura):

- **HTTP 500 o 502: 100%** — el servidor procesa el request completo hasta llegar a RENAPER y falla ahí
- **Errores de red: 0** — el servidor Node.js nunca cayó ni rechazó conexiones
- **Latencia p50 < 10ms** — el stack propio (Fastify + auth + rate limit) es muy rápido
- **Latencia p99 < 1s** — el tiempo extra viene del intento de conexión a RENAPER antes del fallo

---

## 11. Ejemplo de salida completa anotada

```
=================================================================
  STRESS TEST — face-match-node
  RPS objetivo : 200                        ← 200 requests por segundo
  Duración     : 60s                        ← durante 60 segundos = 12.000 requests
  Endpoint     : http://localhost:3100/v3/face-match/
  API Key      : fide-dev... (primeros 8 chars)
  Run ID       : lt_1794343790              ← usar este ID para consultar la DB
  DB           : ./data/fide-facematch.db
  Concurrencia : 400 slots simultáneos      ← hasta 400 requests en vuelo al mismo tiempo
=================================================================

  [DB] Tabla load_test_results lista en ./data/fide-facematch.db
  Iniciando test... presionar Ctrl+C para interrumpir

  Test completado: 12,001 requests en 60.02s (200.0 rps reales)
  Guardando 12,001 registros en SQLite...        ← batch write al final del test
  Registros guardados correctamente.

=================================================================
  STRESS TEST — RESUMEN DE KPIs
  Run ID   : lt_1794343790
  Timestamp: 2026-11-10 17:50:51
=================================================================

  CONFIGURACIÓN
    RPS objetivo      : 200
    Duración objetivo  : 60 s
    Duración real      : 60.02 s

  THROUGHPUT
    Requests enviados  :   12,001
    RPS real           :   199.95           ← el rate limiter fue preciso (±0.05 rps)
    Llegaron al server :   12,001  (100.0%) ← cero errores de red = servidor estable
    HTTP 200           :        0  (0.0%)   ← esperado: RENAPER no tiene credenciales

  LATENCIA (ms) — sobre 12,001 respuestas con tiempo medido
    Mínimo             :        1 ms        ← requests rechazados rápido (rate limit, auth)
    Mediana (p50)      :        2 ms        ← el stack propio de Fastify tarda 2ms
    Promedio           :       21 ms        ← el promedio sube por los que tocan RENAPER
    p90                :       45 ms        ← 90% en <45ms — muy bueno
    p95                :      112 ms
    p99                :      532 ms        ← 532ms incluye el intento fallido a RENAPER
    Máximo             :      756 ms        ← ningún request llegó al timeout (35s)
    Desv. estándar     :       76 ms        ← distribución no uniforme (dos clusters: fast/slow)

  DISTRIBUCIÓN HTTP STATUS
       500  : 10,802  ( 90.0%)  ████...     ← RENAPER falla después de conectar
       502  :  1,199  ( 10.0%)  ████        ← RENAPER no conecta directamente

  DISTRIBUCIÓN API STATUS (campo 'Status' en body JSON)
         ERROR  : 12,001  (100.0%)          ← todos devuelven JSON válido — el servidor no crashea

  ERRORES
    Errores de red     :      0  (0.0%)     ← el servidor nunca cayó ni rechazó conexiones
    Timeouts           :      0             ← ningún request tomó más de 35 segundos

  KPIs CLAVE
    Latencia p99       : BUENO (<1s p99)    ← clasificación automática
    Throughput         : 200.0 rps reales / 200 objetivo   ← 100% del objetivo alcanzado
    Alcance            : 100.0% de requests llegaron al servidor
```

**Conclusión de este resultado:** La infraestructura Node.js/Fastify/SQLite soporta 200 RPS de manera estable. Con credenciales RENAPER reales, la latencia p99 estará dominada por el RTT a RENAPER (típicamente 500ms–3s dependiendo de la carga del servicio externo).

---

*DOC-OPS-008 · v1.0 · 2026-11-10 · CONFIDENCIAL — USO INTERNO · Fidelitas S.A.*
