Bases de datos en Linux: PostgreSQL en VPS pequeño — Ajustes que previenen OOM

¿Te fue útil?

Alquilaste un VPS pequeño porque eres responsable. Luego PostgreSQL se come la máquina a las 03:17, el kernel ejecuta el OOM killer,
y tu “plan de alta disponibilidad” resulta ser una disculpa en Slack y una mañana muy despierta.

La buena noticia: la mayoría de los eventos de falta de memoria (OOM) en servidores Postgres pequeños son autoinfligidos. La mejor noticia:
las soluciones son aburridas, medibles y repetibles—si dejas de tratar la memoria como un solo mando.

Cómo ocurre el OOM en un VPS pequeño (y por qué Postgres recibe las culpas)

En un VPS pequeño, la “memoria” no es un único depósito. Es un tratado tenso entre la caché de páginas del kernel, la
memoria anónima (heap/stack), los segmentos de memoria compartida, las asignaciones por proceso y lo que el hipervisor decidió
que tu vecino no merece hoy. PostgreSQL añade sus propias categorías: shared buffers, memoria de trabajo por backend,
workers en segundo plano, memoria de mantenimiento y extensiones que no tienen ningún interés en tu presupuesto.

Cuando ocurre un OOM, PostgreSQL suele hacerse notar porque es un proceso grande y de larga vida con muchos hijos. Pero la decisión
de matar la toma el kernel, basada en scoring de badness, cgroups de memoria (si se usan) y qué es más fácil eliminar sin tirar
abajo todo el SO. A veces mata el backend más grande. A veces mata el postmaster y obtienes una caída completa. A veces mata tu agente
de monitorización, que es como disparar la alarma de humo porque el pitido molesta.

El patrón más común en máquinas pequeñas no es “shared_buffers demasiado grande.” Es “concurrencia sin límites más ajustes de memoria
por consulta que se multiplican.” Una sola sentencia puede asignar work_mem varias veces (una por cada nodo sort/hash),
y muchos backends pueden hacerlo simultáneamente. Eso no es un bug. Es matemática.

El segundo patrón común es la combustión lenta: autovacuum no da abasto, las tablas se llenan de bloat, los índices se inflan, las
consultas se vuelven más lentas, y las consultas más lentas mantienen conexiones más tiempo, lo que aumenta la concurrencia y la
presión de memoria. Enhorabuena, inventaste un bucle de retroalimentación.

Broma #1: Si pones max_connections a 2000 en un VPS de 1 GB, no tienes un servidor de base de datos—tienes una lotería temática de memoria.

Hechos interesantes y contexto histórico (lo que todavía muerde)

  • Hecho 1: La arquitectura multiproceso de PostgreSQL (un proceso OS por conexión) data de decisiones tomadas en una época en que los hilos eran menos portables y menos predecibles entre variantes de Unix.
  • Hecho 2: shared_buffers solía recomendarse alto por defecto, pero la caché de páginas de Linux y el mejor comportamiento de IO del kernel desplazaron la práctica hacia “buffers moderados, deja trabajar la caché del SO”.
  • Hecho 3: work_mem se ha entendido mal durante décadas porque es por operación, no por consulta. Una consulta puede usarlo varias veces por backend.
  • Hecho 4: PostgreSQL tiene log_temp_files desde hace tiempo, pero muchos equipos aún no lo activan—así nunca ven que su “misterioso” OOM fue un sort que debería haberse desbordado a disco.
  • Hecho 5: Autovacuum se introdujo para reducir la fatiga del vacuum manual, pero su agresividad por defecto es intencionalmente conservadora para evitar sorpresas—en VPS pequeños suele necesitar ayuda.
  • Hecho 6: El OOM killer del kernel existe para mantener el sistema vivo, no tu servicio. No es un algoritmo de justicia; es triage.
  • Hecho 7: La contabilidad de memoria de PostgreSQL no es centralizada porque está distribuida entre procesos y contextos; por eso “uso de memoria de Postgres” siempre es una estimación, nunca un contador único.
  • Hecho 8: El pooling de conexiones (especialmente transaction pooling) se volvió estándar en Postgres en parte porque “un proceso por conexión” es caro operacionalmente en máquinas pequeñas.

Modelo mental: dónde gasta realmente memoria PostgreSQL

1) Memoria compartida: shared_buffers (y amigos)

shared_buffers es la caché gestionada por Postgres de páginas de tablas e índices. Vive en memoria compartida y se
ve como parte de la huella del postmaster principal, pero lo usan todos los backends. En VPS pequeños, quieres que sea lo bastante
grande para evitar churn constante y lo bastante pequeño para dejar espacio para todo lo demás (incluida la caché de páginas del SO).

Regla práctica: en un VPS de 1–4 GB, un shared_buffers entre 128 MB y 1 GB suele ser sensato. Si vas más alto,
más vale que tengas una razón y mediciones. Una máquina pequeña aún necesita memoria para:

  • sobrecoste por conexión (stacks, contextos de memoria, syscalls)
  • sorts/hashes (work_mem)
  • mantenimiento (maintenance_work_mem)
  • WAL buffers, procesos en segundo plano y memoria de extensiones
  • caché de páginas del kernel (tu acelerador de IO barato)

2) Memoria por backend: el multiplicador silencioso

Cada conexión cliente es un proceso backend. Cada backend tiene un uso de memoria base (de unos pocos MB a decenas de MB,
según ajustes, extensiones, locale y forma de la consulta). Luego las consultas añaden memoria en trozos, a menudo en contextos
de memoria ligados a operadores.

Los ajustes peligrosos son los que parecen inofensivos en aislamiento:

  • work_mem (sorts, hashes)
  • temp_buffers (tablas temporales por sesión)
  • max_parallel_workers_per_gather (las consultas paralelas multiplican la memoria)

Un VPS pequeño no necesita astucia. Necesita predictibilidad. Tu objetivo es limitar el peor caso de memoria, no ganar un benchmark.

3) Memoria de mantenimiento: vacuum, create index y la “reconstrucción sorpresa del fin de semana”

maintenance_work_mem se usa en VACUUM, CREATE INDEX, ALTER TABLE ADD FOREIGN KEY y similares.
En máquinas pequeñas, puedes mantenerlo modesto (64–256 MB) y aún estar bien. La trampa es ejecutar varios trabajos de mantenimiento
a la vez: cada uno puede consumir hasta ese límite. Los workers de autovacuum pueden hacerlo concurrentemente.

4) La caché de páginas del SO: no es opcional, no es “desperdicio”

Linux usará la memoria libre como caché. Esto es bueno. Postgres lee datos, el kernel los cachea y lecturas futuras son más rápidas.
Si ahogas la caché de páginas inflando los ajustes de Postgres, forzarás lecturas de disco y aumentarás la latencia, lo que alarga
las consultas, incrementa la concurrencia y la presión de memoria. Ese es el OOM de combustión lenta.

5) Swap: la salida de emergencia, no una segunda sala de estar

En un VPS pequeño, algo de swap suele ser mejor que nada. Da al kernel un sitio para aparcar páginas frías en lugar de matar
inmediatamente tu base de datos en un pico transitorio. Pero si dejas que el sistema haga thrashing en swap bajo carga, no estás
“sobreviviendo”, estás “agotando tiempos de espera lentamente”.

Guía rápida de diagnóstico: primeras, segundas y terceras comprobaciones

Este es el orden que gana incidentes. No siempre. Pero a menudo.

Primero: confirma que realmente es OOM (y quién murió)

  • Revisa los logs del kernel en busca de mensajes del OOM killer.
  • Comprueba si el postmaster se reinició (logs de recuperación de Postgres).
  • Verifica si systemd/cgroup provocó un kill (a menudo parece OOM pero no lo es).

Segundo: encuentra el multiplicador (conexiones, paralelismo, work_mem)

  • ¿Cuántas conexiones activas en el pico?
  • ¿Algún cambio repentino en max_connections, pooler de conexiones o despliegue de la aplicación?
  • ¿Alguna consulta haciendo sorts/hashes enormes o desbordando archivos temporales?
  • ¿Consultas paralelas activadas en una máquina pequeña?

Tercero: busca el bucle de combustión lenta (bloat, autovacuum retrasado, IO wait)

  • ¿Autovacuum en ejecución? ¿Se está quedando atrás?
  • Síntomas de bloat en tablas/índices (tamaños crecientes, escaneos lentos)?
  • ¿Alta latencia de disco? ¿WAL fsync lento? Eso convierte picos en presión sostenida.

Cuarto: valida lo básico a nivel SO (swap, overcommit, ajustes vm)

  • Que exista swap y esté dimensionado apropiadamente.
  • vm.swappiness no esté fijado a algo dramático sin motivo.
  • Que el overcommit de memoria no te haga pensar que las asignaciones son “gratis”.

Idea parafraseada (atribuida): La esperanza no es una estrategia. — General H. Norman Schwarzkopf, citado a menudo en contextos de operaciones (idea parafraseada)

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son las comprobaciones que realmente ejecuto en un VPS pequeño cuando Postgres es acusado de homicidio. Cada tarea incluye:
comando → salida ejemplo → qué significa → qué decisión tomas.

Tarea 1: Confirmar actividad del OOM killer

cr0x@server:~$ journalctl -k -g -i 'out of memory|oom-killer|Killed process' -n 50
Jan 12 03:17:21 vps kernel: Out of memory: Killed process 24113 (postgres) total-vm:612844kB, anon-rss:348920kB, file-rss:1200kB, shmem-rss:0kB, UID:113 pgtables:1256kB oom_score_adj:0
Jan 12 03:17:21 vps kernel: oom_reaper: reaped process 24113 (postgres), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Significado: El kernel mató un backend de postgres específico. Esto es un OOM real, no “Postgres se cayó”.

Decisión: Pasa inmediatamente a comprobar concurrencia y memoria por backend. Un backend se puso grande.

Tarea 2: Comprobar si el postmaster se reinició (recuperación)

cr0x@server:~$ sudo journalctl -u postgresql -n 80
Jan 12 03:17:22 vps postgresql[1023]: LOG:  database system was interrupted; last known up at 2026-01-12 03:12:05 UTC
Jan 12 03:17:22 vps postgresql[1023]: LOG:  database system was not properly shut down; automatic recovery in progress
Jan 12 03:17:23 vps postgresql[1023]: LOG:  redo starts at 0/5A1C2B0
Jan 12 03:17:24 vps postgresql[1023]: LOG:  database system is ready to accept connections

Significado: Postgres se cayó de forma abrupta (el postmaster murió o recibió SIGKILL). Se ejecutó la recuperación.

Decisión: Trátalo como una interrupción: revisa tasas de error de clientes, tiempo de WAL/recuperación y considera el comportamiento OOM de systemd.

Tarea 3: Ver estado de memoria y swap ahora mismo

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           1.9Gi       1.6Gi        74Mi        28Mi       264Mi       152Mi
Swap:          1.0Gi       612Mi       412Mi

Significado: RAM justa; swap en uso. “Available” es lo que importa para nuevas asignaciones.

Decisión: Si el swap sube bajo carga con picos de latencia, estás en zona de thrash; limita concurrencia y multiplicadores de memoria.

Tarea 4: Identificar rápidamente los mayores consumidores de memoria (RSS)

cr0x@server:~$ ps -eo pid,ppid,user,comm,rss,etime --sort=-rss | head -n 15
  PID  PPID USER     COMMAND   RSS     ELAPSED
24113  1023 postgres postgres  348920  00:03:12
24102  1023 postgres postgres  112540  00:03:11
1023      1 postgres postgres   87420  05:42:19
24098  1023 postgres postgres   80112  00:03:10
1780      1 root     node       62310  02:11:03

Significado: Un backend es enorme. Eso suele ser un sort/hash grande, un plan paralelo o una extensión.

Decisión: Mapea ese PID a la consulta y al usuario; ajusta la consulta o reduce work_mem/max_parallel_workers_per_gather.

Tarea 5: Mapear un PID de backend a su consulta

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT pid, usename, state, wait_event_type, wait_event, now()-query_start AS age, left(query,120) AS q FROM pg_stat_activity WHERE pid=24113;"
  pid  | usename | state  | wait_event_type | wait_event |   age    | q
-------+---------+--------+-----------------+------------+----------+------------------------------------------------------------
 24113 | app     | active |                 |            | 00:03:09 | SELECT ... ORDER BY created_at DESC LIMIT 200000;
(1 row)

Significado: Está realizando un large order-by con un LIMIT enorme. Es una invitación a usar memoria y archivos temporales.

Decisión: Arregla la consulta (índices, límites más pequeños, paginación por clave) y establece statement_timeout como cinturón de seguridad.

Tarea 6: Ver conteo y estado de conexiones

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT state, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;"
   state   | count
-----------+-------
 idle      | 82
 active    | 14
 idle in transaction | 7
(3 rows)

Significado: Demasiadas conexiones idle en un VPS pequeño son muerte por mil cortes de papel.

Decisión: Añade un pooler (pgBouncer), reduce max_connections y mata sesiones “idle in transaction”.

Tarea 7: Comprobar max_connections y ajustes actuales

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SHOW max_connections; SHOW shared_buffers; SHOW work_mem; SHOW maintenance_work_mem; SHOW effective_cache_size;"
 max_connections
-----------------
 300
(1 row)

 shared_buffers
---------------
 512MB
(1 row)

 work_mem
---------
 64MB
(1 row)

 maintenance_work_mem
----------------------
 512MB
(1 row)

 effective_cache_size
----------------------
 1536MB
(1 row)

Significado: En una máquina de 2 GB, max_connections=300 y work_mem=64MB es una receta para OOM si hay picos de concurrencia.

Decisión: Reduce max_connections (usa pooler), baja work_mem (a menudo 4–16MB) y ajusta la memoria de mantenimiento.

Tarea 8: Averiguar si sorts/hashes están desbordando a disco (archivos temporales)

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SHOW log_temp_files;"
 log_temp_files
----------------
 -1
(1 row)

Significado: Estás a ciegas respecto al uso de archivos temporales en los logs.

Decisión: Poner log_temp_files = 0 (log todo) temporalmente durante diagnóstico, o un umbral como 16MB para estado estable.

Tarea 9: Comprobar uso actual de archivos temporales por base de datos

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT datname, temp_files, pg_size_pretty(temp_bytes) AS temp_bytes FROM pg_stat_database ORDER BY temp_bytes DESC;"
  datname  | temp_files | temp_bytes
----------+------------+------------
 appdb    |      18412 | 37 GB
 postgres |          3 | 12 MB
(2 rows)

Significado: Las consultas están desbordando a temp en gran medida. Eso puede estar bien, pero en discos pequeños es dolor de IO y alarga la duración de las consultas.

Decisión: Arregla las consultas/índices peores; no simplemente aumentes work_mem “para evitar temp”. Así compras un OOM.

Tarea 10: Comprobar estado del autovacuum y tuplas muertas

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT relname, n_live_tup, n_dead_tup, last_autovacuum, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;"
    relname     | n_live_tup | n_dead_tup |        last_autovacuum        |       last_autoanalyze
---------------+------------+------------+-------------------------------+------------------------------
 events        |   48210321 |   9123401  | 2026-01-10 02:11:44+00        | 2026-01-10 02:30:02+00
 sessions      |    1192031 |    412120  |                               | 2026-01-08 11:02:19+00
(2 rows)

Significado: Presión de bloat. Una tabla acumula tuplas muertas; otra no ha autovacuumado recientemente.

Decisión: Ajusta umbrales de autovacuum para tablas calientes; asegúrate de que vacuum no esté bloqueado; considera VACUUM manual en ventanas de poco tráfico.

Tarea 11: Comprobar si vacuum está bloqueado por transacciones largas

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT pid, usename, state, now()-xact_start AS xact_age, left(query,120) AS q FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start ASC LIMIT 5;"
  pid  | usename | state |  xact_age  | q
-------+---------+-------+------------+------------------------------------------------------------
 23301 | app     | idle in transaction | 02:14:09 | UPDATE sessions SET ...;
(1 row)

Significado: “Idle in transaction” durante horas impide la limpieza del vacuum y hace crecer el bloat.

Decisión: Mata la sesión, arregla el manejo de transacciones en la app, añade idle_in_transaction_session_timeout.

Tarea 12: Ver la presión de IO y actividad de swap bajo carga

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0 621120  74200  18324 232140    0   18   120   340  450  780 22  8 55 15  0
 3  1 623080  61840  18324 230820    0  120   200  1100  520  910 28 11 39 22  0
 4  1 624904  51220  18324 229110    0  240   180  2500  600 1020 34 12 26 28  0

Significado: Swap-out (so) en aumento más rising IO wait (wa) sugiere thrash y contención de disco.

Decisión: Reduce concurrencia, baja memoria por consulta y asegúrate de que el almacenamiento no esté saturado (WAL en disco lento empeora).

Tarea 13: Comprobar límites por cgroup de memoria (systemd) si aplica

cr0x@server:~$ systemctl show postgresql -p MemoryMax -p MemoryHigh -p OOMPolicy -p ManagedOOMMemoryPressure
MemoryMax=infinity
MemoryHigh=infinity
OOMPolicy=stop
ManagedOOMMemoryPressure=auto

Significado: No hay MemoryMax explícito aquí; systemd aún puede reaccionar vía ManagedOOM dependiendo de los defaults de la distro.

Decisión: Si vas a fijar MemoryMax, hazlo con conocimiento y deja holgura para memoria compartida y necesidades del kernel; si no, tendrás “kills misteriosos”.

Tarea 14: Inspeccionar huge pages / restricciones de memoria compartida

cr0x@server:~$ grep -E 'HugePages|Shmem' /proc/meminfo | head
Shmem:             28672 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0

Significado: No hay huge pages configuradas; la memoria compartida es pequeña por ahora. Eso es típico en VPS pequeños.

Decisión: No persigas huge pages como primer arreglo en máquinas diminutas. Arregla concurrencia y multiplicadores de memoria primero.

Tarea 15: Identificar las formas de consulta peores (top por tiempo rápido y sucio)

cr0x@server:~$ sudo -u postgres psql -d appdb -c "SELECT queryid, calls, mean_exec_time, rows, left(query,100) AS q FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 5;"
 queryid  | calls | mean_exec_time |  rows  | q
----------+-------+----------------+--------+----------------------------------------------------
 91344122 |   112 |       8421.113 | 200000 | SELECT ... ORDER BY created_at DESC LIMIT $1
 11822001 |  9011 |        312.882 |     20 | SELECT ... FROM sessions WHERE user_id = $1
(2 rows)

Significado: Una consulta es lenta y con muchas filas; probablemente produce sorts grandes o hashes amplios.

Decisión: EXPLAIN, indexa y limita. En VPS pequeños, una consulta mala puede ser toda la historia de memoria.

Tarea 16: Verificar swappiness del kernel y política de overcommit

cr0x@server:~$ sysctl vm.swappiness vm.overcommit_memory vm.overcommit_ratio
vm.swappiness = 10
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

Significado: Swappiness conservadora, heurística de overcommit por defecto. No es inherentemente erróneo.

Decisión: Evita valores extremos salvo que entiendas el modo de fallo. En VPS pequeños, la predictibilidad supera al folklore.

El ajuste que evita OOM (qué poner, qué evitar)

Comienza con una verdad dura: tu peor caso debe caber

En máquinas pequeñas, tunear no es “hacerlo rápido.” Es “hacer que no muera.” La velocidad viene de evitar comportamientos patológicos:
demasiados backends, demasiada memoria por backend y transacciones largas que convierten picos en carga sostenida.

Piensa en presupuestos:

  • Presupuesto A: memoria compartida (buffers y overhead)
  • Presupuesto B: baseline por conexión × máximo de conexiones activas
  • Presupuesto C: memoria de trabajo por consulta × operadores pesados concurrentes
  • Presupuesto D: workers de mantenimiento × memoria de mantenimiento
  • Presupuesto E: necesidades del SO + caché de páginas + “incógnitas” (extensiones, libc, TLS, monitorización)

1) Arregla al verdadero villano: el conteo de conexiones

Si estás en un VPS de 1–2 GB y tus aplicaciones se conectan directamente a Postgres con tráfico en ráfagas,
estás jugando a la ruleta. Cada conexión “idle” sigue siendo un proceso con overhead de memoria, y cada ráfaga crea un choque de memoria.

Qué hacer:

  • Usar pgBouncer (transaction pooling a menos que necesites características de sesión).
  • Fijar max_connections a lo que tu RAM puede permitirse, no a lo que tu app puede abrir.
  • Preferir menos conexiones con consultas rápidas a muchas conexiones con consultas lentas.

2) Ajusta work_mem como si lo pagaras (porque lo haces)

En VPS pequeños, un work_mem global de 4–16 MB es un buen punto de partida. Si eso suena pequeño, bien:
obliga a las operaciones grandes a desbordar a disco en vez de asesinar la RAM. El desbordamiento es más lento, pero estar muerto
es más lento aún.

El patrón mejor es defecto bajo, mayor por rol o por sesión para trabajos controlados:
el rol de analytics obtiene más, el rol OLTP menos.

Además: las consultas paralelas aumentan el uso de memoria. Cada worker puede asignar work_mem. En un VPS pequeño, limita
agresivamente el paralelismo o desactívalo si es impredecible para tu carga.

3) Mantén shared_buffers moderado

No pongas shared_buffers al 70% de la RAM porque un blog dijo “la caché es rey.” Linux ya cachea.
Quieres espacio para el SO y para picos por backend.

Puntos de partida prácticos:

  • 1 GB RAM: 128–256 MB shared_buffers
  • 2 GB RAM: 256–512 MB shared_buffers
  • 4 GB RAM: 512 MB–1 GB shared_buffers

Luego mide: ratio de cache hit, IO wait y latencia bajo carga. Si aumentas buffers y el rendimiento no mejora, simplemente
robaste memoria de donde la necesitabas.

4) Autovacuum: ajústalo para que el bloat no se convierta en presión de memoria

Autovacuum en un VPS pequeño debe estar configurado para darse abasto, no para ser educado. Los valores por defecto apuntan a ser
seguros en máquinas muy distintas. Tu VPS pequeño no es muy distinto—está específicamente limitado.

Qué ayuda:

  • Reducir autovacuum_vacuum_scale_factor en tablas calientes; confiar menos en umbrales porcentuales y más en umbrales fijos.
  • Aumentar autovacuum_max_workers solo si el IO puede con ello; si no, solo añades contención.
  • Subir autovacuum_work_mem modestamente (o usar maintenance_work_mem) pero no permitir que múltiples workers se descontrolen.
  • Monitorizar “idle in transaction” y transacciones largas que bloquean la limpieza.

5) Pon guardarraíles en el comportamiento de transacciones y sentencias

La característica de fiabilidad más barata en Postgres es un timeout. Los timeouts convierten “se está comiendo tu servidor lentamente”
en “fallo rápido con logs.” Puedes depurar un fallo. No puedes depurar un OOM después de que el proceso desapareció.

  • statement_timeout: detener consultas desbocadas.
  • idle_in_transaction_session_timeout: parar apps que abren una transacción y se olvidan.
  • lock_timeout: detener montones de consultas por esperas de bloqueo.

6) Swap: tenlo, pero no dependas de él

Un pequeño archivo de swap (igual a la RAM o la mitad, según disco y carga) puede reducir la “muerte instantánea” durante un pico.
No es licencia para ignorar el tuning de memoria. Observa las tasas de swap-in/out; si swap está en churn activo bajo carga, ya estás en zona de peligro.

7) Logging que evita supersticiones

Para evitar futuros juegos de culpas, registra lo suficiente para conectar síntomas con causas:

  • Activa logging de consultas lentas (con log_min_duration_statement).
  • Activa logging de archivos temporales (log_temp_files umbral).
  • Usa pg_stat_statements para identificar repetidores.

Broma #2: Encender los logs no arreglará tu caída, pero mejorará drásticamente tu capacidad de acertar en la próxima incidencia.

Tres micro-historias del mundo corporativo (cómo falla en la vida real)

Micro-historia 1: El incidente causado por una suposición equivocada (“work_mem es por consulta, ¿verdad?”)

Un equipo SaaS de tamaño medio ejecutaba Postgres en un VPS de 2 GB para un panel de analítica “no crítico”. Empezó como proyecto lateral y
acabó siendo “todos lo usan cada mañana.” Así funcionan las cosas.

Un desarrollador subió work_mem a 128 MB para acelerar unas consultas de panel. La suposición fue que una consulta obtiene una
asignación de work_mem. También dejaron max_connections generoso porque “las conexiones son baratas.” En testing
el panel era más rápido. En producción fue más rápido hasta la reunión general del lunes de marketing.

En el pico, decenas de consultas concurrentes del panel corrieron con múltiples nodos sort/hash por consulta. Algunas consultas se paralelizaron.
La memoria subió rápido. El swap se activó. La latencia explotó. Los clientes reintentaron. Las conexiones crecieron. El OOM killer del kernel
empezó a eliminar los backends más gordos, y luego se llevó el postmaster por las buenas.

La solución no fue heroica: reducir el work_mem por defecto, limitar workers paralelos, añadir pgBouncer y establecer
work_mem por rol solo para el rol “analytics” con concurrencia controlada. También añadieron un statement_timeout,
que convirtió “fundir la máquina” en “el panel caduca,” un modo de fallo mucho más sano.

Micro-historia 2: La optimización que salió mal (“Vamos a maximizar shared_buffers y deshabilitar swap”)

Otra compañía movió una herramienta de soporte al cliente a un VPS de 1 GB. La herramienta tenía tráfico modesto pero mucho full-text search y un par
de extensiones. Un ingeniero, intentando ayudar, aplicó un snippet de tuning:
shared_buffers=768MB, effective_cache_size=1GB, y “el swap es malo, desactívenlo.”

La primera semana fue bien. Luego instalaron un agente de seguridad que sumó algo de uso de memoria. Más tarde activaron un job nocturno que ejecutaba
reportes pesados. Con swap deshabilitado, los picos transitorios no tenían a dónde ir. El OOM killer del kernel se disparó antes y más violentamente.

El informe del incidente olía familiar: los gráficos mostraban memoria subiendo hasta caer en picado. El tuning redujo lecturas de disco, pero también
eliminó el espacio de respiración de la caché del SO y quitó el alivio que daba el swap. El sistema se volvió frágil.

Volvieron a buffers moderados, reactivaron un pequeño swap y redujeron work_mem por defecto. El rendimiento mejoró porque el sistema dejó
de hacer thrash y de reiniciarse. Resultó que “arriba” es un estado muy rápido.

Micro-historia 3: La práctica aburrida que salvó el día (“timeouts, pooler y un solo panel”)

Un servicio relacionado con finanzas corría en un VPS de 4 GB con Postgres. Nada sofisticado. Tenían dos hábitos que parecían dolorosamente conservadores:
timeouts estrictos y un pooler con tamaño del pool del servidor intencionalmente bajo.

Un product manager pidió una nueva función “exportar todo”. Ingeniería la implementó como un job en segundo plano y un endpoint para usuarios. El endpoint tenía un
statement_timeout estricto. El job usaba un rol dedicado con mayor work_mem y una cola de baja concurrencia.

El día del lanzamiento, un cliente intentó exportar un dataset masivo en plena hora punta. El endpoint caducó y devolvió un error molesto pero honesto. El job en segundo
plano se encoló y corrió después con recursos controlados. La base de datos se mantuvo estable.

El postmortem fue corto porque no había mucho que investigar. La práctica aburrida—compartimentar recursos y timeouts—impidió que la petición de un cliente se convirtiera
en la caída de todos. Nadie fue promovido por ello, pero nadie recibió una página tampoco.

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: desconexiones aleatorias de Postgres, “server closed the connection unexpectedly”

Causa raíz: El OOM killer del kernel mata backends o postmaster; a veces systemd mata por presión de memoria.

Solución: Confirma con logs del kernel; reduce concurrencia, baja work_mem, añade pgBouncer y asegúrate de que exista swap.

2) Síntoma: uso de swap crece, latencia sube, luego todo caduca

Causa raíz: Presión de memoria provocando thrash en swap; típicamente demasiadas conexiones activas o consultas pesadas concurrentes.

Solución: Limita conexiones activas, aplica timeouts, tunear consultas peores, considera reducir shared_buffers para restaurar respiración de la caché de páginas.

3) Síntoma: conexiones “idle in transaction” se acumulan

Causa raíz: La aplicación fuga transacciones (olvida commit/rollback), mantiene locks y bloquea vacuum.

Solución: Establece idle_in_transaction_session_timeout; arregla el manejo de conexiones/transacciones en la app; monitoriza y mata ofensores.

4) Síntoma: autovacuum siempre en ejecución, consultas empeoran días tras día

Causa raíz: Vacuum no da abasto; el bloat aumenta IO y tiempo de consulta; la concurrencia aumenta; la presión de memoria se mantiene.

Solución: Ajusta autovacuum por tablas calientes; elimina bloqueadores (transacciones largas); programa VACUUM manual en ventanas de baja actividad.

5) Síntoma: disco temporal se llena, luego Postgres da errores por falta de espacio

Causa raíz: Sorts/hashes grandes desbordando a archivos temporales; disco insuficiente; a veces una sola consulta de reporte.

Solución: Arregla consultas/índices; limita tamaños de resultados; habilita logging de archivos temporales; asegura disco adecuado o mueve temp a un volumen más rápido/grande si es posible.

6) Síntoma: rendimiento peor tras “tunear” shared_buffers hacia arriba

Causa raíz: Caché de páginas del SO y memoria por backend hambrientos; aumento de IO wait y consultas más largas.

Solución: Reduce shared_buffers a un valor moderado; mide; céntrate en planes de consulta y en gestión de conexiones.

7) Síntoma: una sola consulta a veces desencadena OOM

Causa raíz: Consulta paralela más work_mem alto; el plan incluye múltiples nodos hambrientos de memoria; a veces filas anchas.

Solución: Baja work_mem; limita workers paralelos; reescribe la consulta; añade índices; establece statement_timeout.

Listas de verificación / plan paso a paso (línea base segura para VPS pequeños)

Paso a paso: estabilizar primero, optimizar después

  1. Prueba OOM: revisa logs del kernel por kills; registra timestamps y PIDs.
  2. Cuenta conexiones: mide pico activo y total; identifica “idle” vs “idle in transaction”.
  3. Instala o configura pgBouncer: apunta a un pool del servidor pequeño y estable.
  4. Reduce max_connections: fuerza al pooler a hacer su trabajo.
  5. Establece valores conservadores: work_mem bajo, maintenance_work_mem moderado, shared_buffers moderado.
  6. Limita paralelismo: fija max_parallel_workers_per_gather (o desactiva si hace falta).
  7. Añade timeouts: statement + idle-in-transaction + lock timeout para cordura.
  8. Activa knobs de observabilidad: slow query log, log_temp_files, pg_stat_statements.
  9. Arregla las 3 mejores consultas: indexa, reescribe o limita. En máquinas pequeñas no necesitas top 30.
  10. Tunea autovacuum en tablas calientes: reduce scale factors; asegúrate de que vacuum no esté bloqueado.
  11. Valida swap: que exista; evita deshabilitarlo; monitoriza swap-in/out.
  12. Prueba carga con concurrencia: verifica que el peor caso se mantenga dentro de la memoria.

Ideas de configuración base (no universales, pero bastante seguras)

Estas son opciones intencionalmente conservadoras para VPS pequeños. Ajusta según medición, no según sensaciones.

  • 1 GB RAM: shared_buffers 128–256MB, work_mem 4–8MB, maintenance_work_mem 64–128MB, max_connections 50–100 (preferir pooler).
  • 2 GB RAM: shared_buffers 256–512MB, work_mem 4–16MB, maintenance_work_mem 128–256MB, max_connections 100–150 (preferir pooler).
  • 4 GB RAM: shared_buffers 512MB–1GB, work_mem 8–16MB, maintenance_work_mem 256MB, max_connections 150–200 (preferir pooler).

Lista operacional: después de cualquier cambio

  • Revisa el comportamiento de conexiones durante tráfico pico.
  • Revisa la generación de archivos temporales y consultas lentas.
  • Revisa actividad de swap y IO wait.
  • Revisa progreso de autovacuum y tuplas muertas.
  • Mantén un plan de reversión: los cambios de configuración son baratos; el tiempo de recuperación no lo es.

Preguntas frecuentes

1) ¿Es shared_buffers la causa principal de OOM en VPS pequeños?

A veces, pero generalmente no. El OOM clásico en VPS pequeños es memoria por backend multiplicada por concurrencia: demasiadas
conexiones más un work_mem generoso, a menudo con consultas paralelas. Buffers moderados rara vez matan un sistema por sí solos;
lo que mata son picos sin límite.

2) ¿Cuál es un work_mem seguro en una caja de 1–2 GB?

Empieza en 4–8MB para cargas OLTP generales. Aumenta por rol para trabajos controlados (como un worker de reporting único), no globalmente.
Recuerda: una consulta puede asignar work_mem varias veces.

3) ¿Debería desactivar el swap para bases de datos?

En VPS pequeños, normalmente no. Una pequeña cantidad de swap puede prevenir OOM abruptos durante picos transitorios. La meta real es
evitar swapping en estado estable; monitoriza swap-in/out y IO wait. Desactivar swap a menudo convierte “lento” en “muerto.”

4) ¿Por qué veo muchas conexiones “idle”? ¿No son inofensivas?

Las conexiones idle aún cuestan memoria y overhead de proceso. En máquinas pequeñas, ese overhead importa. Las conexiones idle también empeoran
las ráfagas porque ya gastaste tu presupuesto base. Usa un pooler y mantén las conexiones del servidor pequeñas y estables.

5) ¿Cómo sé si el OOM fue causado por una consulta específica?

Correlaciona: PID del kill en logs del kernel → pg_stat_activity (si aún está) → logs de la aplicación → logs de consultas lentas.
Añade pg_stat_statements y logging de temp files. Si una forma de consulta domina temp_bytes o mean_exec_time, es un fuerte sospechoso.

6) ¿Debería aumentar effective_cache_size para arreglar rendimiento?

effective_cache_size no asigna memoria; es una pista para el planner. Pónlo en una estimación razonable de memoria disponible para caché
(caché del SO + shared_buffers). No lo trates como una palanca de rendimiento para OOM.

7) ¿El tuning de autovacuum ayuda con OOM?

Indirectamente, sí—frecuentemente mucho. Cuando autovacuum se queda atrás, el bloat aumenta, las consultas se ralentizan, la concurrencia sube y la
presión de memoria se mantiene. Mantener vacuum saludable previene la ruta de “combustión lenta” hacia OOM.

8) ¿Puedo resolver esto solo mejorando el VPS?

Más RAM compra margen, no corrección. Si la carga tiene concurrencia sin límite o consultas desbocadas, llenará cualquier memoria que le des—solo más tarde
y más caro. Ajusta primero, escala con evidencia.

9) ¿Son malas las consultas paralelas en VPS pequeños?

No son malas en sentido moral. Son operativamente impredecibles. El paralelismo puede multiplicar uso de memoria y contención CPU. En VPS pequeños,
limítalo bajo. Si necesitas análisis paralelos grandes, probablemente necesites otra máquina o arquitectura.

10) ¿Cuál es la ganancia más rápida si ya hago swap semanalmente?

Añade un pooler y baja max_connections. Luego reduce work_mem global a algo conservador. Esos dos cambios suelen eliminar eventos OOM
porque controlan el multiplicador.

Próximos pasos que puedes hacer hoy

  1. Ejecuta la guía rápida de diagnóstico: confirma OOM, identifica el multiplicador y comprueba si hay bloat/IO de combustión lenta.
  2. Pon un tope duro en la concurrencia: pgBouncer + reducir max_connections.
  3. Reajusta work_mem a un valor conservador: luego súbelo selectivamente por rol para trabajos controlados.
  4. Añade timeouts: statement + idle-in-transaction + lock timeout. Haz que tus fallos sean rápidos y depurables.
  5. Activa visibilidad de temp files y consultas lentas: para que el próximo incidente tenga evidencias, no folklore.
  6. Tunea autovacuum en tablas calientes: la prevención de bloat es protección de memoria con sombrero de IO.

PostgreSQL en VPS pequeños no es frágil por naturaleza. Es frágil cuando permites que el uso de memoria sea ilimitado. Limítalo, mídelo
y dormirás como alguien que no trata al OOM killer como balanceador de carga.

← Anterior
Almacenamiento Linux: La opción de montaje que puede corromper tus expectativas
Siguiente →
Migración de correo electrónico: el plan sin tiempo de inactividad que no pierde mensajes

Deja un comentario