8 min

Pruebas de carga en Go antes de contratar backend

Usa pruebas de carga en Go para modelar tráfico realista y medir latencia p95, presión de base de datos, memoria y errores antes de contratar.

Pruebas de carga en Go antes de contratar backend

Diez mil usuarios activos mensuales no son un requisito de capacidad. Son una cifra de facturación o analítica. Un servicio en Go puede atender a muchos más si las solicitudes son ligeras y se distribuyen durante el mes, o venirse abajo con unos cientos si cada sesión dispara consultas lentas, cargas de archivos y llamadas a servicios externos.

La pregunta útil es si el backend generado cumple un objetivo de servicio definido bajo el pico de tráfico esperado, con margen suficiente para crecer y soportar un fallo corriente. Puedes responder antes de contratar a un ingeniero de backend, pero solo si la prueba de carga se parece a tu producto y registra al mismo tiempo qué hacen la API, el runtime de Go y PostgreSQL. Una gráfica verde de latencia media no demuestra casi nada.

Esta es la prueba que exigiría antes de decirle a un fundador que un backend generado en Go está preparado para 10.000 usuarios activos mensuales. Produce un resultado repetible de aprobado o suspenso, descubre el primer cuello de botella y separa un problema de capacidad de uno de corrección.

Los usuarios mensuales deben convertirse en solicitudes pico

Convierte la previsión de usuarios en solicitudes por segundo antes de elegir un nivel de carga. Los usuarios activos mensuales ocultan las dos variables que mueven un backend: cuántas sesiones llegan en el intervalo más ocupado y cuánto trabajo crea cada sesión.

Empieza con datos observados si ya existe una beta privada. Cuenta las sesiones de los 15 minutos más ocupados, las solicitudes por sesión y la mezcla de rutas. Si todavía no hay tráfico, escribe las suposiciones para que cualquiera pueda discutirlas. Por ejemplo, supongamos que 10.000 usuarios activos generan ocho sesiones al mes y 15 solicitudes de API por sesión, y que el 20 por ciento del tráfico diario cae en la hora de mayor actividad. Eso produce unas 8 solicitudes por segundo en un día ocupado normal. Un lanzamiento, una notificación, una fecha límite de nómina o una zona horaria compartida pueden multiplicar el pico real.

No conviertas esa aritmética en precisión falsa. Úsala para definir tres niveles:

  • Pico esperado: la carga más alta que prevés ahora.
  • Pico de crecimiento: el doble del pico esperado, salvo que el negocio tenga una previsión mejor.
  • Nivel de estrés: aumenta el tráfico hasta que falle un objetivo de servicio o se sature un recurso.

Las pruebas de pico esperado y crecimiento dicen si el lanzamiento previsto tiene margen. La de estrés muestra qué se rompe primero y cómo perciben el fallo los usuarios. Esa última respuesta importa porque un servicio que rechaza rápido el trabajo sobrante es más sencillo de operar que uno que consume todas las conexiones de la base de datos y bloquea rutas no relacionadas.

Usa un modelo de carga abierto para la prueba de capacidad. Un modelo abierto inicia solicitudes a una tasa de llegada fija incluso cuando las anteriores se ralentizan. Un modelo cerrado con un número fijo de usuarios virtuales suele ocultar el colapso: las respuestas más lentas hacen que esos usuarios envíen menos solicitudes nuevas, por lo que la carga ofrecida baja justo cuando el servicio sufre. La documentación de k6 de Grafana refleja la distinción mediante ejecutores por tasa de llegada e informa de dropped_iterations cuando el generador no puede iniciar el trabajo programado. Considera las iteraciones descartadas un fallo del generador, no un éxito del servidor.

Mantén cada nivel estable durante al menos 30 minutos después del calentamiento. Las pruebas de cinco minutos pasan por alto la rotación de conexiones, los ciclos del recolector de basura, la expulsión de caché, las tareas de fondo y el crecimiento gradual de la memoria. Añade una prueba de resistencia separada de dos horas en el pico esperado cuando pase la prueba corta.

Escribe la conversión en una hoja pequeña y conserva las unidades. Los usuarios mensuales multiplicados por sesiones por usuario y solicitudes por sesión dan las solicitudes mensuales. Divide solo después de repartir el tráfico entre días operativos y la hora más ocupada. Añade reintentos, trabajos de fondo, webhooks y sondeos que la analítica de usuarios quizá no cuente. Un frontend que consulta cada diez segundos puede crear más trabajo de API que los clics que abrieron la página.

Modela las ráfagas por separado del pico estable. El inicio de sesión tras una notificación, el final de una importación o los reintentos tras una interrupción breve pueden concentrar el trabajo en un minuto. Añade una etapa que alcance rápido la tasa de ráfaga esperada, la mantenga lo suficiente para llenar colas y vuelva a la normalidad. El servicio debe recuperarse sin una acumulación creciente ni un reinicio manual. Registra el tiempo de recuperación. Un sistema puede aprobar la prueba estable y seguir siendo inseguro si una ráfaga breve deja bloqueados el pool o los workers.

No multipliques la tasa final por un factor de seguridad arbitrario y lo llames realista. Vincula el margen a una incertidumbre del negocio: error de previsión, campaña prevista, una réplica no disponible o tiempo necesario para añadir capacidad. Prueba cada supuesto del que vayas a depender. Guarda la hoja junto al resultado, porque una ejecución aprobada pierde sentido cuando nadie recuerda qué previsión y qué supuestos de ráfaga produjeron el objetivo.

La mezcla de tráfico debe parecer una sesión real

Una prueba realista conserva la frecuencia de rutas, el tamaño de las cargas, la autenticación, la distribución de los datos, las pausas y la contención de escritura. Golpear /health cien veces por segundo mide el manejador de salud, no la aplicación.

Construye la mezcla a partir de los registros de acceso cuando sea posible. Agrupa rutas por acción de negocio y no por URL literal, porque /projects/123 y /projects/456 tienen la misma forma. Una mezcla plausible para un SaaS inicial podría asignar el 45 por ciento a lecturas de listas y detalles, el 20 por ciento a búsquedas, el 15 por ciento a creaciones o cambios, el 10 por ciento a inicios de sesión y renovación de tokens y el 10 por ciento a exportaciones u otro trabajo pesado. Tus cifras deben salir del flujo del producto, no de este ejemplo.

Usa muchas cuentas y registros de prueba. Reutilizar una sola cuenta puede crear una caché caliente poco realista, serializar actualizaciones sobre una fila o activar límites de tasa que el tráfico real repartiría. Prepara inquilinos pequeños, medianos y grandes. Incluye registros ausentes, entradas inválidas y fallos de autorización, porque las rutas de error consultan la base de datos o asignan cuerpos de respuesta de forma distinta a las rutas correctas.

Pon las cargas grandes y exportaciones largas en su propio escenario si tienen otro objetivo de servicio. Ejecútalas a la vez que el tráfico normal. De lo contrario, la prueba no verá el incidente exacto que notan los usuarios: una clase de exportación ocupa el pool mientras una página sencilla de ajustes espera detrás.

No simules PostgreSQL, el almacenamiento de objetos, las colas ni los servicios externos en la prueba final de capacidad. Una simulación sirve para aislar el coste del manejador, pero elimina las dependencias que probablemente fijan la capacidad. Apunta a un entorno de staging con los mismos tamaños de instancia, ajustes de base de datos, índices, límites de conexión y ruta de red que producción. Los datos saneados con forma de producción son mejores que mil filas semilla idénticas.

Evita probar a través de una caché de distribución si la API no se guarda normalmente en caché. Por el contrario, conserva la caché real en la ruta si producción la usa. El objetivo no es hacer que el backend parezca ocupado, sino reproducir el trabajo que provoca de verdad una solicitud del usuario.

Una prueba k6 ejecutable debe codificar el contrato

Guarda los umbrales y las etapas de tráfico en el control de versiones para que una ejecución no se convierta en una captura interpretada después. k6 trata los umbrales como criterios de aprobado o suspenso y termina con un código distinto de cero cuando fallan, lo que permite usar el resultado como control de una versión.

El siguiente esqueleto ejecuta una sesión mixta a una tasa de llegada, comprueba la semántica de la respuesta y fija límites de latencia separados para lecturas normales y exportaciones pesadas. Sustituye rutas, cargas y objetivos por los valores acordados para tu producto. El código es sencillo a propósito para que cada comprobación fallida corresponda a una acción del usuario.

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const businessErrors = new Rate('business_errors');

export const options = {
  scenarios: {
    expected_peak: {
      executor: 'ramping-arrival-rate',
      startRate: 5,
      timeUnit: '1s',
      preAllocatedVUs: 40,
      maxVUs: 200,
      stages: [
        { target: 10, duration: '5m' },
        { target: 10, duration: '30m' },
        { target: 20, duration: '10m' },
        { target: 20, duration: '30m' },
      ],
    },
  },
  thresholds: {
    'http_req_duration{name:project_list}': ['p(95)<300'],
    'http_req_duration{name:project_create}': ['p(95)<500'],
    'http_req_duration{name:export}': ['p(95)<2000'],
    http_req_failed: ['rate<0.01'],
    business_errors: ['rate<0.005'],
    dropped_iterations: ['count==0'],
  },
};

export function setup() {
  const response = http.post(`${__ENV.BASE_URL}/api/login`, JSON.stringify({
    email: __ENV.TEST_EMAIL,
    password: __ENV.TEST_PASSWORD,
  }), { headers: { 'Content-Type': 'application/json' } });
  check(response, { 'login succeeds': r => r.status === 200 });
  return { token: response.json('token') };
}

export default function (data) {
  const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json' };
  const list = http.get(`${__ENV.BASE_URL}/api/projects?limit=25`, {
    headers,
    tags: { name: 'project_list' },
  });
  businessErrors.add(!check(list, {
    'list status is 200': r => r.status === 200,
    'list has items': r => Array.isArray(r.json('items')),
  }));

  if (Math.random() < 0.25) {
    const create = http.post(`${__ENV.BASE_URL}/api/projects`, JSON.stringify({
      name: `load-${__VU}-${__ITER}`,
    }), { headers, tags: { name: 'project_create' } });
    businessErrors.add(!check(create, { 'create status is 201': r => r.status === 201 }));
  }

  if (Math.random() < 0.03) {
    const runExport = http.post(`${__ENV.BASE_URL}/api/exports`, '{}', {
      headers,
      tags: { name: 'export' },
    });
    businessErrors.add(!check(runExport, { 'export accepted': r => r.status === 202 }));
  }

  sleep(Math.random() * 2 + 1);
}

Los objetivos de ejemplo son puntos de partida, no promesas universales. Fija el p95 por clase de ruta según la demora que tolere el usuario y los requisitos del producto. No juzgues una exportación asíncrona y una solicitud de autocompletado con un único umbral global de 300 ms.

Ejecuta el script desde una máquina que no aloje la aplicación. Confirma que el generador tiene CPU libre y que no hay iteraciones descartadas. Guarda el commit exacto, la configuración del entorno, el identificador de la instantánea de datos, el comando y la salida sin procesar. Sin eso, una comparación posterior depende sobre todo de la memoria y el optimismo.

Calibra el generador antes de confiar en una ejecución larga. Apunta a un manejador pequeño sin trabajo de base de datos, sube la tasa solicitada por encima de la prueba prevista y comprueba que la sostiene sin agotar su CPU, sockets o red. Cuando k6 añade usuarios virtuales o informa de iteraciones descartadas, la máquina de carga puede ser el límite. Distribuye la generación entre máquinas solo si una no puede ofrecer el trabajo requerido y sincroniza sus relojes para alinear las gráficas del servidor y del cliente.

Da a cada ejecución una línea base tranquila. Detén migraciones, importaciones y tareas de staging no relacionadas, salvo que también funcionen durante el pico real. Programa después otra prueba con el trabajo de fondo genuino. El par muestra tanto la capacidad limpia de la API como la capacidad operativa que recibirán los usuarios. Si solo pasa la ejecución tranquila, el lanzamiento depende de una ficción de staging.

Crea un segundo script para un único recorrido de usuario y ejecútalo con un usuario virtual antes de añadir carga. Inspecciona cada respuesta, registro creado y limpieza. Así detectas un token incorrecto, una comprobación siempre verdadera o datos que chocan después de la primera iteración. Un resultado de capacidad no vale nada si el script ejercita una página de error o lee siempre el mismo objeto de caché.

p95 necesita contexto por ruta

Usa la latencia p95 porque los promedios ocultan una minoría lenta, pero nunca la leas sola. Con un p95 de 800 ms, una de cada veinte solicitudes tarda al menos eso, lo que puede hacer que una página con varias solicitudes parezca lenta de forma habitual. El percentil también se vuelve inestable en rutas con pocas muestras, así que informa del número de solicitudes junto a él.

Registra p50, p95, p99, máximo, rendimiento y tasa de error para cada acción de negocio con nombre. p50 muestra el comportamiento normal, p95 funciona como control práctico del servicio y p99 expone la cola sin dejar que un máximo aislado domine la conversación. Separa por código de estado. Una respuesta 500 rápida no debe mejorar la historia de latencia.

Mide la duración del manejador en el servidor además de la duración observada por el cliente. La diferencia incluye la apertura de conexiones, proxies, tiempo de red y transferencia de la respuesta. Si el p95 del cliente sube mientras el del manejador permanece estable, busca fuera del manejador. Si ambos suben y aumenta la espera de la base de datos, probablemente la solicitud está esperando una conexión o una consulta.

Mantén separados calentamiento y estado estable. En un binario Go desplegado no hay compilación, pero las cachés frías, conexiones nuevas, inicialización diferida y autoescalado pueden distorsionar los primeros minutos. Los usuarios también sufren ese comportamiento en frío, por lo que debe quedar como resultado aparte.

Los promedios siguen sirviendo para contabilizar recursos. El tiempo total de base de datos dividido por llamadas puede descubrir una consulta moderadamente lenta y muy frecuente. No puede sustituir un objetivo de percentil. El sector suele confundir latencia y capacidad: la latencia describe cuánto tardó el trabajo terminado, mientras la capacidad describe cuánto trabajo ofrecido sostiene el servicio sin colas o errores crecientes. Una latencia buena con una tasa obtenida baja no prueba capacidad.

Define el fallo antes de ejecutar. Yo suspendería una prueba de lanzamiento si una ruta crítica no cumple su p95, los fallos HTTP inesperados superan la tasa acordada, fallan comprobaciones de negocio, se descartan iteraciones programadas o un recurso permanece saturado. Aprobar cuatro de cinco criterios es suspender con datos de diagnóstico útiles.

Las esperas de conexión revelan colas ocultas

Mantén comprobable el código Go
Koder.ai exporta el backend generado para guardar junto a él la prueba de carga y las métricas.

Instrumenta database/sql antes de probar, porque la latencia de aplicación no dice si PostgreSQL va lento o la aplicación espera para acceder a él. DB.Stats() de Go informa de OpenConnections, InUse, Idle, WaitCount y WaitDuration, además de contadores de cierres. Expórtalos al sistema de métricas cada pocos segundos.

func recordDBStats(ctx context.Context, db *sql.DB, g GaugeSet) {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            s := db.Stats()
            g.Set("db_open_connections", float64(s.OpenConnections))
            g.Set("db_in_use_connections", float64(s.InUse))
            g.Set("db_idle_connections", float64(s.Idle))
            g.Set("db_wait_count_total", float64(s.WaitCount))
            g.Set("db_wait_seconds_total", s.WaitDuration.Seconds())
        }
    }
}

Calcula el cambio de los contadores acumulativos en la ventana estable. Si WaitCount aumenta, hubo solicitudes esperando una conexión libre. El cambio de WaitDuration dividido por el de WaitCount da la espera media del pool en ese intervalo. Grafica InUse frente a MaxOpenConnections; una línea plana en el límite con esperas crecientes indica saturación.

No respondas elevando SetMaxOpenConns hasta que la gráfica se vea mejor. Ese arreglo popular desplaza la cola a PostgreSQL y puede aumentar la contención, la memoria y la latencia. Averigua primero por qué siguen ocupadas las conexiones: consultas lentas, transacciones abiertas durante llamadas de red, lectura fila a fila o llamadas olvidadas a Rows.Close(). Dimensiona después el pool dentro del presupuesto de conexiones de la base de datos para todas las réplicas y workers.

La documentación de Go dice que un valor no positivo de SetMaxOpenConns deja el pool sin límite. Es un valor predeterminado peligroso en producción cuando varias réplicas pueden abrir conexiones a la vez. Fija un límite explícito, configura de forma deliberada el tiempo inactivo y la vida útil y reserva capacidad para migraciones, administración y trabajo de fondo.

Mide aparte la duración de las transacciones. Un manejador puede responder en 200 ms mientras una limpieza diferida o una transacción perdida retiene la conexión mucho más. Las métricas del pool muestran la presión; las trazas o el tiempo de transacción muestran al responsable.

Las pruebas de consultas lentas deben venir de PostgreSQL

Activa pg_stat_statements en el entorno de prueba y toma instantáneas antes y después de cada ejecución. La documentación de PostgreSQL lo describe como un registro de estadísticas de planificación y ejecución para sentencias normalizadas. Su vista contiene llamadas, filas, tiempo total y medio, actividad de bloques y bloques temporales. Es una evidencia mucho mejor que adivinar por la consulta que apareció en una traza.

Usa diferencias entre instantáneas porque la vista es acumulativa. Restablécela solo en una base aislada, pues el reinicio borra evidencia de otro trabajo. Esta consulta encuentra las sentencias que consumieron más tiempo de ejecución en una ventana limpia:

SELECT
  queryid,
  calls,
  round(total_exec_time::numeric, 1) AS total_ms,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  rows,
  shared_blks_read,
  temp_blks_written
FROM pg_stat_statements
WHERE calls > 20
ORDER BY total_exec_time DESC
LIMIT 20;

El tiempo total encuentra consultas frecuentes que dominan el trabajo de base de datos. El tiempo medio encuentra sentencias lentas individualmente. Ninguno da el p95 porque pg_stat_statements agrega llamadas; usa trazas o histogramas cuando importe la cola de una sentencia. Esta es otra distinción que suele perderse: una consulta lenta puede tener una media alta, una cola alta o simplemente un coste total enorme por su frecuencia. Cada caso requiere un arreglo distinto.

Para las sentencias principales, ejecuta EXPLAIN (ANALYZE, BUFFERS) con parámetros seguros y representativos fuera de la prueba cronometrada. ANALYZE ejecuta la sentencia, así que encierra las que cambien datos en una transacción que reviertas o usa una copia desechable. Busca diferencias fuertes entre filas estimadas y reales, bucles repetidos, escaneos secuenciales sobre tablas grandes y selectivas, ordenaciones que pasan a disco y muchos bloques compartidos leídos.

El código generado suele crear un patrón N+1 que parece correcto con datos semilla: obtiene 25 proyectos y luego ejecuta una consulta de propietario y otra de recuento por proyecto. A diez solicitudes por segundo, ese endpoint puede generar más de 500 sentencias por segundo antes de que funcione otra ruta. La reparación puede ser un join, un WHERE id = ANY($1) por lotes o un recuento precalculado. Aumentar el pool deja intacto el desperdicio.

Registra también esperas por bloqueos. Una consulta rápida de forma aislada puede detenerse bajo cambios concurrentes del mismo inquilino, cuenta o secuencia. Si la latencia salta solo en el escenario de escritura, examina esperas activas y límites de transacción en lugar de añadir un índice por reflejo.

La memoria debe estabilizarse con carga constante

Planifica capacidad antes de generar
Define límites del pool, tiempos y fallos observables en el modo de planificación antes de cambiar código.

Juzga la memoria por su forma en el tiempo, no por un único pico. Un proceso Go que sube durante el calentamiento y luego oscila alrededor de un nivel estable se comporta de forma distinta a uno cuya base después de la recolección sube durante dos horas.

Registra memoria residente, heap asignado, objetos del heap, número de goroutines, frecuencia y pausas del recolector y tasa de asignación. La memoria del contenedor importa porque el sistema operativo mata el proceso por su límite, no por el heap de Go. Compara memoria después de recolecciones con carga similar. Así se elimina buena parte del diente de sierra normal y aparece la retención.

Expón las métricas estándar del runtime de Go o un endpoint protegido de perfilado en staging. Toma un perfil de heap cerca del inicio y otro al final de la prueba, y compara los lugares de asignación retenida con go tool pprof. Toma también un perfil de goroutines. Un recuento creciente puede revelar solicitudes detenidas en canales, cuerpos de respuesta sin cerrar o trabajo de fondo sin cancelación.

No configures GOMEMLIMIT exactamente al límite del contenedor. El proceso necesita memoria para pilas de goroutines, mapeos ejecutables, búferes del controlador de base de datos y otras asignaciones fuera del heap. Deja margen y demuéstralo con las cargas más grandes. Una prueba con JSON diminuto dice poco de un endpoint que lee una carga de 20 MB en memoria.

Fuerza los casos incómodos: cuerpos máximos aceptados, resultados grandes, clientes cancelados, tiempos agotados y exportaciones repetidas. Verifica que la memoria vuelva al terminar el trabajo. Vigila también CPU, porque una recolección intensa puede mantener la memoria bajo el límite mientras destruye la latencia.

Una condición de aprobado útil combina techo y tendencia. Exige que la memoria residente quede con seguridad por debajo del límite en el pico de crecimiento y que la base posterior a la recolección y las goroutines dejen de crecer durante la prueba larga. No existe un porcentaje seguro universal. Elige margen según el reinicio de la plataforma, la variación del tráfico y si otra réplica puede absorber un reinicio.

Los fallos incluyen respuestas erróneas y sobrecarga

Cambia código con un punto seguro
Crea una instantánea antes de corregir presión del pool, consultas en cascada o memoria sin límite.

Cuenta por separado fallos de transporte, estados HTTP, tiempos agotados, panics y respuestas incorrectas. http_req_failed captura solicitudes HTTP fallidas según el callback de k6, pero un 200 con una lista vacía, un cargo duplicado o un registro ausente sigue siendo un fallo. Por eso el ejemplo emite business_errors a partir de comprobaciones de contenido.

Etiqueta los rechazos esperados, como entradas inválidas o límites intencionados, para que no contaminen la tasa inesperada. Después valida su contrato: estado correcto, cuerpo acotado y rechazo rápido. Un sistema sobrecargado no debería tardar 30 segundos en devolver 503.

Busca en los registros recuperación de panics, errores de fecha límite del contexto, demora al adquirir conexión, fallos de serialización de PostgreSQL y consultas canceladas. Agrupa por causa estable, no por el mensaje completo, para que los identificadores no creen miles de categorías. Guarda trazas representativas de la primera aparición y de la cola de alta latencia.

Ejecuta una prueba de degradación después de la capacidad limpia. Reduce conexiones disponibles, añade latencia controlada a una dependencia externa o reinicia una réplica mientras continúa el tráfico. Hazlo solo en el entorno aislado. El objetivo es confirmar que los tiempos límite, la cancelación y los controles de salud contienen el fallo y no dejan que las colas consuman todos los recursos.

Configura el servidor HTTP de forma explícita. La documentación de net/http de Go indica que valores cero o negativos de ReadTimeout, WriteTimeout e IdleTimeout pueden significar ausencia de límite, según el campo. Los servicios generados suelen llamar a http.ListenAndServe con valores predeterminados sin tomar la decisión. Un http.Server propio, fechas límite por solicitud y cuerpos acotados evitan que clientes lentos y dependencias bloqueadas retengan recursos para siempre.

Revisa la corrección después de la ejecución. Cuenta los registros creados, verifica idempotencia cuando los clientes reintentaron, comprueba que los trabajos de fondo terminaron una sola vez y que no quedó estado parcial tras solicitudes fallidas. Las pruebas de carga han encontrado en mis proyectos más errores de trabajo duplicado que revisiones de código muy ingeniosas.

La primera limitación decide la contratación

Puedes lanzar sin ingeniero de backend cuando el sistema pasa repetidamente las pruebas esperada y de crecimiento, la prueba larga alcanza una base de memoria estable, las colas de base de datos permanecen controladas y el equipo puede explicar el primer fallo de estrés. Una ejecución verde por suerte no es evidencia. Ejecuta el mismo commit al menos tres veces e investiga variaciones grandes.

Guarda un registro compacto por ejecución:

  • Commit y entorno, incluidos tamaños de réplicas y configuración de base de datos.
  • Escala del conjunto de datos, mezcla de tráfico, tasas de llegada y duración.
  • p95 y p99 por ruta, rendimiento conseguido, fallos de negocio e iteraciones descartadas.
  • Picos de CPU y memoria, tendencia posterior a recolección y tendencia de goroutines.
  • Esperas del pool, SQL principal por tiempo total, esperas de bloqueo y punto de ruptura observado.

Contrata ayuda de backend antes de lanzar si nadie puede explicar esperas crecientes, memoria retenida, contención de bloqueos o escrituras incoherentes. Contrata si la única persona capaz de ejecutar la prueba no puede cambiar con seguridad el código generado. Es una carencia de propiedad, no un umbral de solicitudes por segundo.

Una prueba fallida no justifica automáticamente un puesto a tiempo completo. Un índice ausente, una consulta N+1 o una exportación sin límite pueden ser arreglos acotados. Fallos repetidos en diseño de transacciones, observabilidad, cancelación y despliegue indican trabajo de ingeniería continuo. La diferencia está entre encontrar un defecto y descubrir que nadie es responsable del comportamiento del sistema.

Koder.ai puede generar y exportar un backend en Go, desplegarlo y conservar instantáneas para volver atrás, pero generar no elimina la planificación de capacidad. Guarda el script de carga y los cambios de observabilidad junto al código para que cada cambio importante tenga que cumplir el mismo contrato.

No prometas al negocio que 10.000 usuarios mensuales son seguros. Promete una tasa medida, una mezcla de rutas, un objetivo de latencia, un presupuesto de errores y un marco de recursos. Cuando cambie el producto, cambia esas entradas y vuelve a ejecutar.

Preguntas frecuentes

¿Puede un backend Go soportar 10.000 usuarios activos mensuales?

A menudo sí, pero los usuarios mensuales no describen la carga. Convierte la previsión en solicitudes pico por segundo y una mezcla de rutas, y prueba límites explícitos de latencia, errores, base de datos y memoria.

¿Cuántas solicitudes por segundo equivalen a 10.000 usuarios mensuales?

No hay conversión fija. Necesitas sesiones por usuario, solicitudes por sesión, la proporción de tráfico en el intervalo más ocupado y cualquier evento que reúna usuarios.

¿Qué latencia p95 es aceptable para una API Go?

Fija objetivos por acción del usuario, no por lenguaje o framework. Las lecturas interactivas pueden necesitar unos cientos de milisegundos y el trabajo de fondo otro objetivo, pero el equipo debe decidirlo antes de ver resultados.

¿Cuánto debe durar una prueba de carga del backend?

Mantén cada nivel estable al menos 30 minutos después del calentamiento y ejecuta una prueba más larga en el pico esperado. Las pruebas cortas pueden omitir rotación de conexiones, memoria retenida, trabajo de fondo y crecimiento de colas.

¿Debo usar usuarios virtuales o tasa de llegada?

Usa una tasa de llegada para afirmar capacidad porque sigue ofreciendo trabajo cuando el servicio se ralentiza. Un modelo con usuarios fijos puede reducir solicitudes durante la demora y ocultar el crecimiento de colas.

¿Cómo detecto el agotamiento del pool de Go?

Exporta DB.Stats() y vigila InUse, OpenConnections, WaitCount y WaitDuration. Contadores de espera crecientes con conexiones en uso en el máximo indican que las solicitudes esperan el pool.

¿Debo aumentar el pool SQL cuando hay esperas?

No hasta saber por qué siguen ocupadas las conexiones y cuánta capacidad tiene PostgreSQL. Un pool mayor puede mover la cola a la base de datos y empeorar la contención.

¿Cómo encuentro consultas PostgreSQL lentas durante la carga?

Toma instantáneas anterior y posterior de pg_stat_statements y ordena las diferencias por tiempo total y medio. Usa trazas o histogramas para la cola porque la vista agregada no ofrece p95 por consulta.

¿Cómo sé si un servicio Go pierde memoria?

Ejecuta una prueba estable y compara memoria después de recolecciones con carga similar. Una base que sigue subiendo, sobre todo con objetos o goroutines crecientes, exige comparar perfiles.

¿Cuándo debe una startup contratar un ingeniero de backend?

Contrata cuando los fallos revelen una necesidad continua de propiedad sobre base de datos, observabilidad, concurrencia, corrección u operaciones. Un índice aislado quizá no exija un puesto, pero un comportamiento inexplicable bajo carga sí.

Related posts