> La lista completa de artículos está en [llms.txt](https://docs.f3cloud.com/llms.txt). Cada página tiene versión en Markdown, basta con añadir .md a su dirección. Esta página en Markdown: [https://docs.f3cloud.com/es/web/site-errors-502-504-403-500.md](https://docs.f3cloud.com/es/web/site-errors-502-504-403-500.md).

# Errores 502, 504, 403 y 500 en Nginx – qué significan y cómo solucionarlos

Qué significan los errores 502 Bad Gateway, 504 Gateway Time-out, 403 Forbidden y 500 Internal Server Error en un servidor con Nginx y cómo solucionarlos. La cadena que va del navegador a la aplicación, diagnóstico con curl, el registro error.log y systemctl, análisis de las causas más frecuentes con líneas reales del registro, ajuste de los tiempos de espera y de los permisos de acceso. Probado en Debian 12 con Nginx 1.22.1 y PHP 8.2.

Abres tu sitio web y, en lugar de la página, ves una pantalla blanca con el texto **502 Bad Gateway**, **504 Gateway Time-out**, **403 Forbidden** o **500 Internal Server Error**. El número del error asusta, pero en realidad indica con bastante precisión en qué tramo del camino entre el navegador y el sitio se ha roto algo, y la causa real queda anotada en el registro de Nginx en una sola línea. En este artículo veremos cómo es ese camino, aprenderemos a encontrar la causa en un par de minutos y solucionaremos uno por uno los cuatro errores con los ejemplos que se dan con más frecuencia. Hemos reproducido todos los ejemplos en un servidor de pruebas con Debian 12, y la versión de Nginx en el momento de la prueba era la **1.22.1** y la de PHP, la **8.2.34**.

***

## Cómo es el camino del navegador al sitio

Cuando un visitante abre una página, su navegador envía una petición al servidor web, es decir, a Nginx. Si hace falta una imagen normal o una página HTML ya hecha, Nginx simplemente toma el archivo del disco y lo entrega. Pero la mayoría de los sitios no son solo archivos. La página hay que construirla, por ejemplo consultando una base de datos, y de eso se encarga un programa aparte al que se llama **backend**. Puede ser PHP-FPM, que ejecuta los scripts de WordPress, o una aplicación en Python o Node.js que escucha en su puerto. En este esquema, Nginx hace de portero. Recibe la petición, se la pasa al backend, espera la respuesta y se la reenvía al navegador. Por eso dos de los cuatro errores llevan en el nombre la palabra «pasarela» (Gateway), porque en ese momento Nginx hace de pasarela entre el visitante y la aplicación.

La primera cifra del código indica de qué lado está el fallo. El código **403** pertenece a la familia 4xx, es decir, a las peticiones que no se pueden atender tal como llegaron. Nginx entendió perfectamente lo que quería el navegador, pero se negó a dar la respuesta, porque no hay nada que mostrar o porque no tiene permisos para leer los archivos necesarios. Los códigos de la familia 5xx indican un fallo del lado del servidor. El código **500** dice que algo se ha roto al procesar la petición, casi siempre en el propio script o en la aplicación. El código **502** significa que Nginx no ha recibido del backend una respuesta coherente, por ejemplo porque el backend no está en marcha. El código **504** significa que el backend está conectado, pero no ha tenido tiempo de responder en el plazo fijado. Por eso, ante cualquiera de estos errores, primero hay que entender en qué eslabón de la cadena se ha producido el corte.

El código nombra el eslabón, pero no la causa, y la causa hay que buscarla en el registro. Veamos, por ejemplo, una situación en la que se ha detenido PHP-FPM. El navegador sigue llamando a Nginx, este intenta pasar la petición al socket de PHP-FPM, pero no puede conectarse, y por eso él mismo dibuja una página 502 corta con el número de su versión abajo, y en el registro anota a qué no ha podido conectarse exactamente. Si, en cambio, PHP-FPM funciona, pero el script se ha caído con un error fatal, Nginx recibe de él una respuesta con el código 500 y el cuerpo vacío y la reenvía tal cual, y el texto del error lo dibuja ya el propio navegador. Vimos las dos situaciones en el servidor de pruebas, y por el aspecto de la página se puede adivinar quién la ha generado, Nginx o la aplicación. Aun así, la respuesta exacta está en el registro de errores, al que pasamos ahora.

***

## Qué necesitas

* **Un servidor** con Debian 12 en el que ya funcione Nginx, por ejemplo el plan **DE-RZ9-1**. Los ejemplos del artículo son muy ligeros, y para elegir el plan adecuado para tu sitio te ayudará el artículo sobre [VPS](https://docs.f3cloud.com/es/vps/cloud-server-types/vps-hosting.md). Si todavía no tienes servidor, contrátalo siguiendo la [guía](https://docs.f3cloud.com/es/client-area/order-vps.md).
* **Conexión por SSH**, por ejemplo con [Termius](https://docs.f3cloud.com/es/vps/termius-connection.md). Los comandos se ejecutan como `root`.
* **Nginx y PHP-FPM**. Cómo instalarlos lo describimos en el artículo sobre la [instalación de Nginx](https://docs.f3cloud.com/es/web/nginx-installation.md). PHP-FPM solo hace falta para los ejemplos con PHP.
* **Un sitio o una aplicación** en el que veas el error.

> **Direcciones y nombres en los ejemplos.**
>
> En el servidor de pruebas, cada ejemplo funcionaba en su propio puerto, por eso en los comandos y en las capturas del terminal, en lugar del dominio, aparece una dirección del tipo `127.0.0.1:8091`. A tu sitio llámalo por el dominio, por ejemplo `https://example.com`. Las carpetas `errors-demo-…` y el servicio `errors-app` también están inventados para el artículo, y tú tendrás tus propios nombres. Las capturas del navegador están hechas en esos mismos sitios de prueba, y el dominio `example.com` de la barra de direcciones es ficticio.

***

## Paso 1. Averigua el código de respuesta

Empieza con el comando `curl`. Accede al sitio igual que el navegador, pero muestra solo lo que el servidor ha respondido de verdad. El parámetro `-I` pide que envíe solo las cabeceras de la respuesta, y el código está en la primera línea.

```bash
# Mostramos las cabeceras de la respuesta, en la primera línea estará el código
curl -I https://example.com
```

Si solo necesitas la línea con el código, añade el parámetro `-s` y pasa la salida al comando `head`.

```bash
# Solo la primera línea de la respuesta
curl -sI https://example.com | head -1
```

Una línea del tipo `HTTP/1.1 502 Bad Gateway` confirma que el error lo ha dado el servidor, y no tu red ni el navegador. De paso, en la cabecera `Server` se ve la versión de Nginx.

***

## Paso 2. Lee el registro de errores de Nginx

Nginx anota cada rechazo en el archivo `/var/log/nginx/error.log`. Abre el registro en modo seguimiento y luego actualiza en el navegador la página con el error. La nueva línea aparecerá en ese mismo momento.

```bash
# Seguimos el registro, para salir pulsa Ctrl+C
tail -f /var/log/nginx/error.log
```

> **Las líneas antiguas no molestan.**
>
> El comando `tail -f` primero muestra las últimas diez líneas del registro y solo después espera las nuevas. Para que las entradas antiguas no distraigan, añade el parámetro `-n 0`, como en la captura de abajo.

<figure><img src="https://docs.f3cloud.com/img/errors-diagnosis.png" alt="El comando curl -I muestra 502 Bad Gateway y tail -f muestra la línea connect() failed (111: Connection refused) de error.log"><figcaption><p>El código de respuesta y la línea del registro que explica la causa</p></figcaption></figure>

Analizar una línea así no es difícil. Tomemos la que obtuvimos cuando la aplicación que hay detrás de Nginx estaba detenida.

```text
2026/10/21 14:53:33 [error] 124894#124894: *3 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8098/", host: "example.com"
```

* **La hora y el nivel** al principio de la línea. La palabra entre corchetes indica la gravedad de la entrada, y nosotros vimos `[error]` y `[crit]`.
* **La esencia del problema** justo después del número de proceso. Aquí es `connect() failed (111: Connection refused)`, es decir, Nginx no ha podido conectarse porque el puerto está cerrado.
* **client** muestra quién ha hecho la petición, **server** y **host** muestran a qué sitio, y **request** contiene la propia petición.
* **upstream** muestra a dónde intentó Nginx pasar la petición exactamente. Es la dirección del backend, y sugiere qué programa revisar a continuación.

El registro es común a todos los sitios del servidor, por eso, con mucho tráfico, conviene buscar la entrada que necesitas por la dirección de la página.

```bash
# Últimas entradas en las que aparece la dirección buscada
grep '/slow' /var/log/nginx/error.log | tail -n 5
```

Puedes leer más sobre cómo consultar los logs y dónde están en el artículo sobre los [logs en Linux](https://docs.f3cloud.com/es/linux/linux-logs.md).

***

## Paso 3. Comprueba el programa que hay detrás de Nginx

Si en la línea del registro aparece la palabra `upstream`, a continuación hay que averiguar qué pasa con el backend. Para una aplicación que funciona como servicio de systemd, comprueba su estado y los últimos mensajes. En lugar de `myapp`, pon el nombre de tu servicio, y para PHP será `php8.2-fpm`.

```bash
# Estado del servicio
systemctl status myapp --no-pager

# Últimos 50 mensajes del servicio
journalctl -u myapp -n 50 --no-pager

# Quién escucha en el puerto que necesitas, en lugar de 8098 pon el tuyo
ss -tlnp | grep 8098
```

En la línea **Active** debe poner `active (running)`. Los valores `failed` e `inactive (dead)` significan que el programa se ha detenido, y la causa suele contarla el registro del servicio. El último comando muestra los programas que escuchan en los puertos. Una respuesta vacía significa que nadie escucha en ese puerto y que Nginx no tiene a quién pasarle las peticiones. Cómo gestionar los servicios y leer sus logs se describe con detalle en el artículo sobre los [servicios systemd](https://docs.f3cloud.com/es/linux/systemd-services.md).

<figure><img src="https://docs.f3cloud.com/img/errors-502-app-failed.png" alt="systemctl status muestra failed, journalctl muestra FileNotFoundError y el comando ss no encuentra nada en el puerto 8098"><figcaption><p>El servicio está en estado failed, la causa está en el registro y nadie escucha en el puerto 8098</p></figcaption></figure>

En la captura, el servicio `errors-app` se cayó nada más arrancar. En el registro se ve la causa, `FileNotFoundError`, es decir, a la aplicación le faltaba el archivo de configuración. Con el número del error y la línea del registro, ya se puede pasar al análisis de la causa concreta.

***

## Error 502 Bad Gateway

La página 502 significa que Nginx ha aceptado la petición, pero no ha podido obtener respuesta del backend. Se ve como una página blanca corta con el número de versión de Nginx abajo.

<figure><img src="https://docs.f3cloud.com/img/errors-502-browser.png" alt="Página 502 Bad Gateway con el texto nginx/1.22.1"><figcaption><p>Así se ve el error 502 en el navegador</p></figcaption></figure>

Hemos reproducido las tres causas más frecuentes, y cada una tiene su propia línea en el registro.

### La aplicación no está en marcha

Esta es la causa más sencilla y más frecuente. En la configuración de Nginx, el sitio pasa las peticiones a la aplicación.

```nginx
location / {
    proxy_pass http://127.0.0.1:8098;
}
```

Si la aplicación se ha detenido, se ha caído o no ha llegado a arrancar después de reiniciar el servidor, en su puerto no hay nadie. El registro contiene entonces la línea que ya conoces.

```text
2026/10/21 14:53:33 [error] 124894#124894: *3 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8098/", host: "example.com"
```

El código `111: Connection refused` significa literalmente que la conexión ha sido rechazada, porque el puerto está cerrado. En nuestro ejemplo, la causa la mostró el registro del servicio, que viste en la captura anterior. La aplicación no arrancó porque habíamos renombrado su archivo de configuración. Devuelve el archivo a su sitio e inicia el servicio.

```bash
# Devolvemos el archivo de configuración a su sitio
mv /opt/errors-demo/config.json.old /opt/errors-demo/config.json

# Iniciamos el servicio y comprobamos la respuesta del sitio
systemctl start errors-app
curl -I http://127.0.0.1:8091
```

<figure><img src="https://docs.f3cloud.com/img/errors-502-app-fixed.png" alt="Tras iniciar el servicio, curl -I devuelve 200 OK y curl muestra la respuesta de la aplicación"><figcaption><p>El servicio está en marcha y el sitio vuelve a responder con el código 200</p></figcaption></figure>

Para que este error no se repita después de cada fallo, encarga a systemd que levante la aplicación él mismo. Para ello, añade dos líneas en la sección `[Service]` del archivo del servicio.

```ini
[Service]
# Reiniciamos la aplicación si se ha caído
Restart=on-failure
# Esperamos tres segundos antes de reiniciar
RestartSec=3
```

Después vuelve a leer la configuración y activa el arranque automático al iniciar el servidor.

```bash
# Aplicamos los cambios del archivo del servicio
systemctl daemon-reload

# Activamos el arranque junto con el servidor
systemctl enable errors-app
```

La comprobación fue muy gráfica. Finalizamos el proceso a la fuerza con el comando `systemctl kill -s KILL errors-app`. Justo después, el sitio respondía con el código 502, y tres segundos más tarde systemd volvió a arrancar la aplicación y el sitio se abrió de nuevo. Fíjate en que, antes de activar el arranque automático, en la línea **Loaded** de la captura ponía `disabled`. Significa que, después de reiniciar el servidor, el servicio no arranca por sí solo.

### PHP-FPM, socket no encontrado

En los sitios con PHP, Nginx pasa las peticiones no a un puerto, sino a un socket, es decir, un archivo especial a través del cual se comunica con PHP-FPM. La ruta está indicada en la directiva `fastcgi_pass`.

```nginx
location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
```

Si ese archivo no existe, Nginx responde 502, y el registro lo dice con todas las letras.

```text
2026/10/21 14:53:33 [crit] 124894#124894: *5 connect() to unix:/run/php/php8.1-fpm.sock failed (2: No such file or directory) while connecting to upstream, client: 127.0.0.1, server: example.com, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock:", host: "example.com"
```

El código `2: No such file or directory` significa que el archivo del socket no existe. Casi siempre hay dos causas. O bien en la configuración está el nombre de otra versión de PHP, por ejemplo el de una guía para PHP 8.1, y tú tienes la 8.2, o bien el servicio PHP-FPM está detenido y su socket ha desaparecido con él. Mira qué sockets hay en el servidor.

```bash
# Lista de sockets de PHP-FPM
ls /run/php/
```

<figure><img src="https://docs.f3cloud.com/img/errors-502-php-socket.png" alt="curl muestra 502, en error.log aparece php8.1-fpm.sock failed (2: No such file or directory) y en la carpeta /run/php está php8.2-fpm.sock"><figcaption><p>En la configuración está php8.1-fpm.sock, pero en el servidor solo existe php8.2-fpm.sock</p></figcaption></figure>

En nuestro ejemplo, en la carpeta estaban `php8.2-fpm.sock` y `php-fpm.sock`. Corrige el nombre en `fastcgi_pass`, comprueba la configuración y aplícala.

```bash
# Cambiamos la versión en el nombre del socket
sed -i 's/php8.1-fpm.sock/php8.2-fpm.sock/' /etc/nginx/sites-available/example.com

# Comprobamos la configuración y la aplicamos
nginx -t && systemctl reload nginx
```

> **El archivo php-fpm.sock.**
>
> En Debian, junto al socket de una versión concreta hay también `php-fpm.sock`. Es un enlace al socket de la versión de PHP instalada, y en nuestro servidor apuntaba a `php8.2-fpm.sock`. Si indicas en `fastcgi_pass` precisamente este, no tendrás que cambiar el nombre en la configuración al cambiar de versión de PHP. Comprobamos que un sitio con esa ruta responde con normalidad.

Si el nombre es correcto y aun así no hay socket, es que se ha detenido el propio PHP-FPM. Lo detuvimos con el comando `systemctl stop php8.2-fpm` y obtuvimos el mismo error 502 con la línea `connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory)`. El comando `systemctl status php8.2-fpm` mostró `inactive (dead)`, y después de iniciar el servicio el sitio volvió a funcionar.

<figure><img src="https://docs.f3cloud.com/img/errors-502-php-stopped.png" alt="Tras detener php8.2-fpm el sitio responde 502, en el registro no hay archivo de socket, systemctl status muestra inactive (dead) y tras start el sitio funciona"><figcaption><p>PHP-FPM detenido, no hay socket, y tras iniciar el servicio el sitio vuelve a abrirse</p></figcaption></figure>

Si el servicio no arranca, la causa la mostrará `journalctl -u php8.2-fpm -n 50 --no-pager`. Y después de cambiar la configuración de PHP-FPM, compruébala con el comando `php-fpm8.2 -t`, como mostraremos más adelante.

### PHP-FPM, sin acceso al socket

A veces el socket está en su sitio, pero Nginx no puede usarlo. En el registro aparece entonces otro código.

```text
2026/10/21 14:57:38 [crit] 133790#133790: *56 connect() to unix:/run/php/errors-fpm.sock failed (13: Permission denied) while connecting to upstream, client: 127.0.0.1, server: example.com, request: "HEAD / HTTP/1.1", upstream: "fastcgi://unix:/run/php/errors-fpm.sock:", host: "127.0.0.1:8099"
```

<figure><img src="https://docs.f3cloud.com/img/errors-502-sock-denied.png" alt="curl devuelve 502, en el registro connect() to unix:/run/php/errors-fpm.sock failed (13: Permission denied) y ls muestra que el propietario del archivo del socket es root"><figcaption><p>El socket está en su sitio, pero pertenece a root y Nginx no puede conectarse a él</p></figcaption></figure>

El código `13: Permission denied` significa que Nginx no tiene permisos sobre ese archivo. Nginx funciona con el usuario `www-data`, y el acceso al socket lo determinan los parámetros `listen.owner`, `listen.group` y `listen.mode` de la configuración del pool de PHP-FPM. Según la documentación de PHP, si no se indica propietario, el socket recibe el usuario con el que se ha iniciado el servicio y los permisos `0660`. En nuestro ejemplo creamos un pool aparte, `errors`, en el archivo `/etc/php/8.2/fpm/pool.d/errors.conf` y no indicamos propietario.

```ini
[errors]
user = nobody
group = nogroup
listen = /run/php/errors-fpm.sock
pm = ondemand
pm.max_children = 2
pm.process_idle_timeout = 10s
```

El servicio se inicia como `root`, por eso el socket recibió el propietario `root` y los permisos `srw-rw----`, y `www-data` no pertenece a ese grupo. Añade al pool dos líneas con el propietario.

```bash
# Vamos a la carpeta con la configuración de los pools
cd /etc/php/8.2/fpm/pool.d

# Permitimos que el usuario de Nginx trabaje con el socket
printf 'listen.owner = www-data\nlisten.group = www-data\n' >> errors.conf

# Comprobamos la configuración y reiniciamos PHP-FPM
php-fpm8.2 -t && systemctl restart php8.2-fpm

# Vemos el propietario del socket y comprobamos el sitio
ls -l /run/php/errors-fpm.sock
curl http://127.0.0.1:8099/
```

<figure><img src="https://docs.f3cloud.com/img/errors-502-sock-fixed.png" alt="Tras añadir listen.owner y listen.group el socket pertenece a www-data y curl devuelve la respuesta de PHP"><figcaption><p>El socket pertenece a www-data y el sitio responde</p></figcaption></figure>

> **Hace falta restart, no reload.**
>
> Cuando, después de cambiar la configuración, hicimos `systemctl reload php8.2-fpm`, el propietario del socket siguió siendo el mismo, `root`, y el error 502 no desapareció. El socket cambió de propietario solo después de `systemctl restart php8.2-fpm`.

> **Comprueba siempre la configuración antes de recargar.**
>
> Acostúmbrate a ejecutar `nginx -t` antes de cada comando `reload`. Si hay un error en la configuración, la comprobación lo mostrará, y Nginx, que está funcionando, seguirá respondiendo con la configuración antigua. Para PHP-FPM existe la misma comprobación, el comando `php-fpm8.2 -t`.

***

## Error 504 Gateway Time-out

La página 504 indica que el backend está vivo y ha aceptado la petición, pero tarda demasiado en preparar la respuesta. Nginx espera un tiempo limitado y, si no la recibe, interrumpe él mismo la espera. El tiempo de espera lo fija la directiva `proxy_read_timeout`, que por defecto son 60 segundos, y en PHP-FPM desempeña el mismo papel `fastcgi_read_timeout`. Un detalle importante de la documentación de Nginx es que este tiempo se cuenta entre dos lecturas consecutivas, y no para toda la respuesta completa. Si el backend envía la siguiente porción de datos más a menudo que una vez cada 60 segundos, la conexión no se corta.

Para verlo con tus propios ojos, añadimos a la aplicación de pruebas la dirección `/slow`, que responde solo al cabo de 75 segundos, como un informe pesado o una consulta larga a la base de datos. La página estuvo abriéndose exactamente un minuto, y después apareció el error.

<figure><img src="https://docs.f3cloud.com/img/errors-504-browser.png" alt="Página 504 Gateway Time-out con el texto nginx/1.22.1"><figcaption><p>El error 504 aparece después de un minuto de espera</p></figcaption></figure>

El comando `curl` con el parámetro `-w` permite medir el tiempo y ver el código de un vistazo.

```bash
# Mostramos el código de respuesta y el tiempo de espera
curl -s -o /dev/null -w '%{http_code} en %{time_total} s\n' http://127.0.0.1:8093/slow
```

En el registro apareció entonces esta entrada.

```text
2026/10/21 15:04:14 [error] 144934#144934: *81 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 127.0.0.1, server: example.com, request: "GET /slow HTTP/1.1", upstream: "http://127.0.0.1:8098/slow", host: "127.0.0.1:8093"
```

<figure><img src="https://docs.f3cloud.com/img/errors-504-log.png" alt="curl devuelve 504 a los 60 segundos y en error.log aparece upstream timed out (110: Connection timed out) while reading response header from upstream"><figcaption><p>Código 504 a los 60,06 segundos y la línea upstream timed out en el registro</p></figcaption></figure>

Las palabras `while reading response header` significan que Nginx se conectó con éxito, envió la petición y esperaba el comienzo de la respuesta. La medición mostró 60,06 segundos, es decir, Nginx cortó la espera exactamente en el valor por defecto.

Conviene arreglar el 504 empezando por la causa. Averigua qué es exactamente lo que tarda, por ejemplo una consulta pesada a la base de datos, una llamada a un servicio ajeno o un informe grande, y acelera eso. Si la tarea es larga por naturaleza, por ejemplo una exportación o una importación, aumenta el tiempo de espera para la dirección que corresponda. Añade en el bloque `location` la directiva `proxy_read_timeout`.

```nginx
location / {
    proxy_pass http://127.0.0.1:8098;
    # Esperamos la respuesta de la aplicación hasta dos minutos
    proxy_read_timeout 120s;
}
```

Comprueba la configuración y aplícala, y luego repite la petición.

```bash
# Comprobamos la configuración y la aplicamos
nginx -t && systemctl reload nginx

# Repetimos la petición
curl -s -o /dev/null -w '%{http_code} en %{time_total} s\n' http://127.0.0.1:8093/slow
```

<figure><img src="https://docs.f3cloud.com/img/errors-504-fixed.png" alt="Con proxy_read_timeout 120s la misma petición devuelve 200 a los 75 segundos"><figcaption><p>La misma petición ahora devuelve 200 a los 75 segundos</p></figcaption></figure>

En los sitios con PHP-FPM se hace lo mismo, solo que la directiva se llama `fastcgi_read_timeout` y va en el bloque `location ~ \.php$`, junto a `fastcgi_pass`. Lo comprobamos con un script que responde a los 70 segundos. Con la configuración por defecto daba 504 exactamente a los 60 segundos, y con la línea `fastcgi_read_timeout 120s;` respondió con el código 200 a los 70 segundos. La línea del registro tenía entonces este aspecto.

```text
2026/10/21 15:07:46 [error] 153814#153814: *89 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 127.0.0.1, server: example.com, request: "GET /slow.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock", host: "127.0.0.1:8096"
```

> **El tiempo de espera de Nginx no cancela el límite de PHP.**
>
> PHP tiene su propio límite de tiempo de ejecución de un script, el parámetro `max_execution_time`. En el archivo `/etc/php/8.2/fpm/php.ini` de nuestro servidor es de 30 segundos. Ejecutamos un script que ocupaba el procesador sin parar, y PHP lo detuvo exactamente a los 30 segundos. Como respuesta llegó no un 504, sino un error 500, y en el registro apareció la línea `PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/errors-demo-php/busy.php on line 3`. Un script que simplemente esperaba con la llamada `sleep(70)` no quedó sujeto a esta restricción, porque, según la documentación de PHP, el tiempo de espera de las llamadas al sistema en Linux no se cuenta. Si tu script choca justo con el límite de PHP, sube `max_execution_time`, y no solo el tiempo de espera de Nginx.

***

## Error 403 Forbidden

La página 403 significa que Nginx ha entendido la petición, pero se ha negado a responderla. En la práctica hay dos causas. O bien en la carpeta no hay nada que mostrar, o bien Nginx no tiene permisos para leer el archivo. En el registro estos casos se distinguen fácilmente, por eso empieza siempre por él.

<figure><img src="https://docs.f3cloud.com/img/errors-403-browser.png" alt="Página 403 Forbidden con el texto nginx/1.22.1"><figcaption><p>Así se ve el error 403 en el navegador</p></figcaption></figure>

### No hay archivo de índice

Cuando un visitante abre la dirección de una carpeta, por ejemplo `https://example.com/`, Nginx busca en ella la página principal. El nombre del archivo lo fija la directiva `index`, que por defecto es `index.html`. Si no existe ese archivo, Nginx podría mostrar la lista de archivos de la carpeta, pero por defecto esa opción está desactivada (`autoindex off`), y por eso responde 403.

```text
2026/10/21 14:53:33 [error] 124894#124894: *7 directory index of "/var/www/errors-demo-403a/" is forbidden, client: 127.0.0.1, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
```

La frase `directory index of … is forbidden` significa justamente que en la carpeta no hay archivo de índice y que la lista de archivos está prohibida. Mira qué hay en la carpeta del sitio.

```bash
# Vemos el contenido de la carpeta del sitio
ls -la /var/www/errors-demo-403a
```

<figure><img src="https://docs.f3cloud.com/img/errors-403-noindex.png" alt="curl devuelve 403, en el registro directory index of /var/www/errors-demo-403a/ is forbidden y ls muestra una carpeta vacía"><figcaption><p>En la carpeta del sitio no hay página principal, por eso Nginx responde 403</p></figcaption></figure>

La carpeta estaba vacía. Lo más habitual es que ocurra cuando el sitio se ha subido a una carpeta equivocada o se ha olvidado subir el propio archivo de la página principal. Comprueba que la directiva `root` apunta a la carpeta donde está realmente el sitio y que en ella hay un archivo con el nombre de la directiva `index`. Si en un sitio PHP es `index.php`, asegúrate de que está indicado en `index index.html index.php;`. Después de subir el archivo, la respuesta cambia al instante y no hace falta recargar Nginx.

```bash
# Creamos la página principal
echo '<h1>El sitio funciona</h1>' > /var/www/errors-demo-403a/index.html

# Comprobamos la respuesta
curl -sI http://127.0.0.1:8094/ | head -1
```

Después de esto, el servidor respondió `HTTP/1.1 200 OK`.

Si lo que necesitas es justamente la lista de archivos, por ejemplo para una carpeta con archivos comprimidos, actívala con la directiva `autoindex on;` en el bloque `location` que corresponda. Comprobamos que en ese caso Nginx devuelve la página `Index of /` con la lista de archivos de la carpeta.

> **La lista de archivos la ve todo el mundo.**
>
> Con `autoindex on`, cualquier visitante verá los nombres de todos los archivos de la carpeta y podrá descargar cada uno de ellos. Actívalo solo para las carpetas que estés dispuesto a mostrar por completo.

### Sin permisos sobre el archivo

En el segundo caso, el archivo está en su sitio, pero Nginx no puede leerlo. El registro lo comunica así.

```text
2026/10/21 14:53:33 [error] 124894#124894: *8 open() "/var/www/errors-demo-403b/index.html" failed (13: Permission denied), client: 127.0.0.1, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
```

El código `13: Permission denied` significa una denegación de acceso a nivel de Linux. Para entender de quién son los permisos que importan, averigua con qué usuario funciona Nginx y luego mira los permisos de cada elemento de la ruta con el comando `namei`.

```bash
# Con qué usuario funciona Nginx
grep ^user /etc/nginx/nginx.conf

# Permisos de cada carpeta y archivo de la ruta
namei -l /var/www/errors-demo-403b/index.html
```

<figure><img src="https://docs.f3cloud.com/img/errors-403-perm.png" alt="En el registro open() index.html failed (13: Permission denied), en nginx.conf user www-data y namei muestra los permisos -rw------- root root del archivo index.html"><figcaption><p>Nginx funciona como www-data, y el archivo está cerrado para todos excepto root</p></figcaption></figure>

Nginx funciona con el usuario `www-data`, y el archivo tiene los permisos `-rw-------` y pertenece a `root`, es decir, solo puede leerlo el propietario. Para `www-data` es un usuario ajeno y no tiene acceso. Esta situación la obtuvimos creando el archivo como `root` con los permisos `600`. Para un sitio sirve una regla sencilla. A las carpetas se les pone `755`, a los archivos `644`, y como propietario se pone el usuario del servidor web. Cómo funcionan los permisos y qué significan esas cifras se explica en el artículo sobre los [permisos de acceso en Linux](https://docs.f3cloud.com/es/linux/linux-permissions.md).

```bash
# Pasamos el sitio al usuario del servidor web
chown -R www-data:www-data /var/www/errors-demo-403b

# 755 para las carpetas, 644 para los archivos
find /var/www/errors-demo-403b -type d -exec chmod 755 {} +
find /var/www/errors-demo-403b -type f -exec chmod 644 {} +
```

<figure><img src="https://docs.f3cloud.com/img/errors-403-perm-fixed.png" alt="Tras chown y chmod el archivo index.html tiene los permisos -rw-r--r-- y el propietario www-data, y curl devuelve 200 OK"><figcaption><p>El archivo se puede leer y el sitio responde con el código 200</p></figcaption></figure>

Para leer el archivo, a Nginx le basta el permiso `r` para «otros». Comprobamos que, con el propietario `root` y los permisos `644`, el sitio también se abre, y que el cambio de propietario hace falta para que el propio sitio pueda guardar los archivos que se suben.

> **No arregles el 403 con permisos 777.**
>
> Los permisos `777` permiten a todos los usuarios del servidor leer y modificar el archivo, y el error, en efecto, desaparece. Pero con él desaparece también la protección del sitio, por eso es mejor no aplicar ese consejo de internet. Pon como propietario al usuario `www-data` y asigna `755` y `644`.

### Sin permisos sobre una carpeta de la ruta al archivo

A veces el propio archivo es accesible para todos y el error sigue ahí. Para llegar al archivo, Nginx debe pasar por cada carpeta de la ruta, y para eso `www-data` necesita el permiso `x` en cada una de ellas. Basta con que una sola carpeta esté cerrada. El registro en este caso se ve algo distinto, porque no contiene la palabra `open()`.

```text
2026/10/21 15:08:10 [error] 156517#156517: *109 "/var/www/errors-demo-403c/public/index.html" is forbidden (13: Permission denied), client: 127.0.0.1, server: example.com, request: "GET / HTTP/1.1", host: "127.0.0.1:8095"
```

En nuestro ejemplo, el archivo `index.html` tiene los permisos `644`, pero la carpeta que está por encima está cerrada. Para encontrarla te ayudará el mismo comando `namei`.

```bash
# Buscamos la carpeta cerrada en la ruta al archivo
namei -l /var/www/errors-demo-403c/public/index.html
```

<figure><img src="https://docs.f3cloud.com/img/errors-403-dir.png" alt="namei muestra los permisos drwx------ de la carpeta errors-demo-403c y, tras chmod 755, el sitio responde 200 OK"><figcaption><p>La carpeta errors-demo-403c está cerrada para todos excepto root, y tras chmod 755 el sitio se abre</p></figcaption></figure>

En la salida, la carpeta `errors-demo-403c` tiene los permisos `drwx------`, y las demás carpetas están abiertas. Ábrela para lectura y acceso.

```bash
# Permitimos entrar en la carpeta a todos los usuarios
chmod 755 /var/www/errors-demo-403c
```

Esta situación es típica de los sitios que están en una carpeta cerrada, por ejemplo la personal. En nuestro servidor, la carpeta `/root` tiene los permisos `700`, por eso no conviene colocar un sitio en ella. Para los sitios usa `/var/www`.

***

## Error 500 Internal Server Error

Al código 500 se le llama el más impreciso, porque significa simplemente «algo ha ido mal al procesar la petición». Por eso aquí es especialmente importante mirar el registro. Hemos reproducido dos causas que se encuentran con más frecuencia que otras. La primera es de PHP y la segunda, del propio Nginx.

### Error fatal en PHP

Si un script PHP se cae, PHP-FPM responde con el código 500. Por defecto, PHP no muestra al visitante el texto del error, y en nuestro caso el parámetro `display_errors` en `/etc/php/8.2/fpm/php.ini` estaba desactivado y `log_errors`, activado. El cuerpo de la respuesta estaba vacío, por eso el mensaje lo dibujó el propio navegador, y no Nginx.

<figure><img src="https://docs.f3cloud.com/img/errors-500-php-browser.png" alt="Página de error propia del navegador con el texto HTTP ERROR 500"><figcaption><p>Con una respuesta vacía, el navegador muestra su propia página con el código HTTP ERROR 500</p></figcaption></figure>

En cambio, todo lo que quería decir PHP llega al registro de Nginx.

```text
2026/10/21 15:09:26 [error] 156517#156517: *123 FastCGI sent in stderr: "PHP message: PHP Fatal error:  Uncaught Error: Call to undefined function mb_strlen() in /var/www/errors-demo-php/page.php:3
Stack trace:
#0 {main}
  thrown in /var/www/errors-demo-php/page.php on line 3" while reading response header from upstream, client: 127.0.0.1, server: example.com, request: "GET /page.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock:", host: "127.0.0.1:8096"
```

<figure><img src="https://docs.f3cloud.com/img/errors-500-php-log.png" alt="curl devuelve 500 y en error.log aparece la entrada FastCGI sent in stderr PHP Fatal error Call to undefined function mb_strlen()"><figcaption><p>Código 500 y la línea del registro con el texto del error de PHP</p></figcaption></figure>

La entrada empieza con las palabras `FastCGI sent in stderr`, y tras ellas viene el propio mensaje de PHP. Hay que leerlo así. El tipo de error es `PHP Fatal error`, luego va la esencia, `Call to undefined function mb_strlen()`, es decir, se ha llamado a una función que no existe, y al final el archivo y la línea donde ocurrió. En nuestro ejemplo, la función `mb_strlen()` pertenece al módulo **mbstring**, que no está en una instalación nueva de PHP. Este módulo figura en la lista que recomendamos instalar para WordPress en el artículo sobre la [instalación de Nginx](https://docs.f3cloud.com/es/web/nginx-installation.md). Instala el módulo que falta.

```bash
# Instalamos el módulo mbstring para PHP
apt install -y php-mbstring
```

Después de la instalación no reiniciamos nada, y la página funcionó enseguida.

<figure><img src="https://docs.f3cloud.com/img/errors-500-php-fixed.png" alt="Tras apt install php-mbstring la página devuelve 200 OK y el número 11"><figcaption><p>El módulo está instalado y la página devuelve 200 y el resultado de la función</p></figcaption></figure>

Otra causa frecuente del 500 en PHP es un error de sintaxis después de editar un archivo. Puedes comprobar el archivo en el servidor con el comando `php -l`, que también te dirá en qué línea está el error.

```bash
# Comprobamos la sintaxis del archivo
php -l /var/www/errors-demo-php/parse.php
```

```text
PHP Parse error:  syntax error, unexpected token "echo", expecting "," or ";" in parse.php on line 3
Errors parsing parse.php
```

> **No actives la visualización de errores en un sitio en producción.**
>
> El parámetro `display_errors` se puede activar para ver el error directamente en el navegador, pero en un sitio en producción no conviene hacerlo. El texto del error contiene rutas de archivos del servidor, y lo verá cualquier visitante. El registro de Nginx da la misma información, pero solo a ti.

### Bucle de redirecciones internas

La segunda causa no tiene nada que ver con la aplicación, es un error en la configuración del propio Nginx. Veamos una configuración en la que, para cualquier dirección en la que no haya un archivo adecuado, se entrega la página principal `/index.html`.

```nginx
root /var/www/errors-demo-cycle;

location / {
    try_files $uri /index.html;
}
```

El último parámetro de `try_files` funciona como una redirección interna. Nginx empieza el procesamiento de nuevo, ya para la dirección `/index.html`, y vuelve a caer en el mismo `location`. Si el archivo `index.html` existe, todo termina bien. Pero si no existe, por ejemplo porque se ha olvidado subir la compilación del sitio o porque `root` apunta a otro sitio, Nginx vuelve a redirigir la petición a `/index.html`, y así en círculo. Según la documentación de Nginx, ese bucle se interrumpe después de diez intentos, y el cliente recibe un error 500.

<figure><img src="https://docs.f3cloud.com/img/errors-500-cycle-browser.png" alt="Página 500 Internal Server Error con el texto nginx/1.22.1"><figcaption><p>Aquí el error 500 lo dibujó el propio Nginx, por eso abajo se ve su versión</p></figcaption></figure>

En el registro, la entrada lo dice directamente.

```text
2026/10/21 14:53:33 [error] 124894#124894: *11 rewrite or internal redirection cycle while internally redirecting to "/index.html", client: 127.0.0.1, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
```

La frase `rewrite or internal redirection cycle` significa un bucle de redirecciones, y entre comillas está la dirección a la que Nginx iba en círculo. Comprueba si existe ese archivo en la carpeta indicada en `root` y, si hace falta, súbelo.

```bash
# Vemos qué hay en la carpeta del sitio
ls -la /var/www/errors-demo-cycle

# Ponemos el archivo en su sitio
echo '<h1>El sitio funciona</h1>' > /var/www/errors-demo-cycle/index.html

# Comprobamos la respuesta
curl -sI http://127.0.0.1:8097/ | head -1
```

<figure><img src="https://docs.f3cloud.com/img/errors-500-cycle.png" alt="curl devuelve 500, en el registro rewrite or internal redirection cycle while internally redirecting to /index.html y, tras crear index.html, la respuesta es 200 OK"><figcaption><p>El bucle se detiene en cuanto el archivo necesario aparece en la carpeta del sitio</p></figcaption></figure>

Tras crear el archivo, la respuesta pasó a ser `HTTP/1.1 200 OK`. No hizo falta recargar Nginx, porque lo que había que arreglar no era la configuración, sino el contenido de la carpeta.

***

## Preguntas frecuentes

<details>

<summary>upstream prematurely closed connection while reading response header from upstream</summary>

Esta línea del registro acompaña al error 502. Nginx se conectó a la aplicación y envió la petición, pero la aplicación cortó la conexión sin enviar respuesta. Por ejemplo, podría haberse caído justo durante el procesamiento de la petición. Mira el registro del servicio con el comando `journalctl -u nombre-servicio -n 50 --no-pager`, y ahí debería estar la causa. Lo reprodujimos con una línea del tipo `upstream prematurely closed connection while reading response header from upstream, … request: "GET /close HTTP/1.1", upstream: "http://127.0.0.1:8098/close"`.

</details>

<details>

<summary>upstream sent too big header while reading response header from upstream</summary>

El error 502 aparece cuando la aplicación envía cabeceras de respuesta demasiado largas, por ejemplo cookies enormes. Nginx lee la primera parte de la respuesta en un búfer, cuyo tamaño por defecto es de una página de memoria, es decir, 4 u 8 KB. Si las cabeceras no caben en él, la respuesta se considera no válida. Lo comprobamos con una cabecera de 9000 caracteres. Aumenta el búfer en el bloque `location`.

```nginx
location / {
    proxy_pass http://127.0.0.1:8098;
    # Un búfer algo mayor para las cabeceras de respuesta
    proxy_buffer_size 16k;
    proxy_buffers 8 16k;
}
```

No se puede omitir la línea `proxy_buffers 8 16k;`. Cuando añadimos solo `proxy_buffer_size 16k;`, `nginx -t` informó del error `"proxy_busy_buffers_size" must be less than the size of all "proxy_buffers" minus one buffer`, y esa configuración no se puede aplicar. Con las dos líneas la comprobación pasó, y la página se abrió con el código 200.

</details>

<details>

<summary>nginx -t muestra [emerg] después de cambiar la configuración</summary>

Significa que hay un error en el archivo de configuración y que Nginx no aplicará esa configuración. Mientras no ejecutes `systemctl reload nginx`, el sitio sigue funcionando con la configuración antigua, por eso ejecuta siempre `nginx -t && systemctl reload nginx`, y no solo el comando `reload`. El texto del mensaje indica qué es lo que falla. Por ejemplo, `"proxy_busy_buffers_size" must be less than the size of all "proxy_buffers" minus one buffer` habla de tamaños de búfer incoherentes en el bloque `location`.

</details>

<details>

<summary>El sitio muestra un error, pero error.log está vacío</summary>

Lo más probable es que ese sitio tenga su propio registro de errores. Se fija con la directiva `error_log` en el bloque `server`, y entonces las entradas van al archivo indicado y no al `error.log` general. Lo comprobamos. Con la directiva `error_log /var/log/nginx/errors-demo.log;` en el bloque del sitio, la línea sobre el error 403 apareció solo en ese archivo. Puedes encontrar todos los registros que conoce Nginx con el comando de abajo.

```bash
# Mostramos todas las directivas error_log de toda la configuración
nginx -T 2>/dev/null | grep -n error_log
```

En nuestro ejemplo, el comando mostró `error_log /var/log/nginx/error.log;` del archivo principal y una línea con la dirección del registro del sitio.

</details>

<details>

<summary>curl: (7) Failed to connect to … port … Couldn't connect to server</summary>

Esto ya no es uno de los cuatro errores. Nginx no ha enviado ninguna respuesta, porque en ese puerto no escucha nadie. En ese caso, el navegador muestra su propia página, que en Chrome es `ERR_CONNECTION_REFUSED`. Recibimos este mensaje al acceder a un puerto que nadie ocupaba, `curl: (7) Failed to connect to 127.0.0.1 port 8090 after 0 ms: Couldn't connect to server`. Comprueba que el propio Nginx está en marcha con el comando `systemctl status nginx` y que los puertos 80 y 443 están abiertos en el [cortafuegos](https://docs.f3cloud.com/es/vps/security/firewall.md).

</details>

<details>

<summary>PHP-FPM registra Maximum execution time of 30 seconds exceeded</summary>

El script ha funcionado más tiempo del que permite el parámetro `max_execution_time`, que por defecto son 30 segundos, y PHP lo ha detenido. En ese caso, Nginx devuelve 500, y no 504. Abre `/etc/php/8.2/fpm/php.ini`, aumenta el valor del parámetro y reinicia el servicio con el comando `systemctl restart php8.2-fpm`. Si el script debe funcionar más de un minuto, no olvides subir también `fastcgi_read_timeout` en la configuración de Nginx.

</details>

***

## Nuestros recursos

**Canal de Telegram:** [https://t.me/f3cloud_news](https://t.me/f3cloud_news)\
**Web del hosting:** [https://f3cloud.com](https://f3cloud.com/es)
