RedirectyRedirectMatch(mod_alias) sirven para el uno a uno;RewriteRule(mod_rewrite) hace falta cuando la decisión depende del host, de https o de la query string.- En
.htaccessel patrón deRewriteRulese compara con la ruta sin barra inicial:^/vieja$no coincide nunca,^vieja$sí. - El orden cuenta: lo específico va antes que lo general, y las redirecciones antes del catch-all de WordPress o de tu front controller.
- Probé los ejemplos con un Apache 2.4.58 real y reproduje tres bucles típicos:
[L]que reprocesa,Redirectcon prefijo y reglas de www contradictorias. .htaccessse lee en cada petición: 200 reglas en un.htaccessbajaron mi prueba de unas 19.600 a unas 4.400 peticiones por segundo; las mismas reglas en la configuración del servidor, no.- Si no tienes acceso a la configuración,
.htaccesses la vía; si lo tienes, elVirtualHostes mejor.
Casi todas las guías de redirecciones en .htaccess son una lista de snippets para copiar. El problema aparece después: pegas el bloque y la web devuelve un 500, o redirige dos veces, o no hace nada porque el servidor ignora el archivo. Y no sabes cuál de las tres cosas pasa porque casi nadie enseña la respuesta real de cada regla.
Aquí cada ejemplo lo ejecuté contra un Apache de verdad y pongo lo que devolvió curl. Cubre la elección entre Redirect y RewriteRule, los flags, https y www, barra final, query strings, 410, el orden de las reglas, los bucles, el coste de leer .htaccess y cómo depurarlo. Al final, lo que hay (y lo que no hay) en el .htaccess de esta web.

Cómo probé los ejemplos
Arranqué un Apache 2.4.58 (Ubuntu) con su propia configuración, AllowOverride All en el DocumentRoot, mod_rewrite, mod_alias, mod_headers y mod_ssl cargados, y un certificado autofirmado para probar http a https de verdad. Los dominios son ficticios (ejemplo.test, viejo.test, nuevo.test) y los resuelvo con curl --connect-to, sin tocar las reglas ni el puerto. Cada comprobación es de este estilo:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://ejemplo.test/ruta/pag?x=1
curl -sIL http://ejemplo.test/ruta/pag # -L sigue los saltos
curl -s -o /dev/null -w '%{num_redirects}\n' -L http://ejemplo.test/ruta/pag
Lo que no probé: otras versiones de Apache, hostings concretos ni el efecto en Google; lo que cito de Google viene de su documentación.
Qué necesita el servidor para leer tus reglas
Antes de escribir una sola redirección, el servidor debe permitir .htaccess en ese directorio, tener cargados los módulos y dejar activos los permisos adecuados. Según lo que falle, el síntoma cambia, y conviene reconocerlo.
| Configuración del directorio | Qué pasó con mi .htaccess | Dónde se ve |
|---|---|---|
AllowOverride None | Ignorado sin avisar: la URL vieja dio 404 | En ningún sitio |
AllowOverride AuthConfig | 500 con una línea Redirect | Redirect not allowed here en el error.log |
Options -FollowSymLinks con AllowOverride All | 403 con una RewriteRule | AH00670: «Options FollowSymLinks and SymLinksIfOwnerMatch are both off…» |
AllowOverride None + AllowOverrideList Redirect RedirectMatch RewriteEngine RewriteRule RewriteCond | Redirect y RewriteRule funcionan; un Header da 500 | Header not allowed here |
La documentación de Apache lo resume así: el valor por defecto de AllowOverride es None y, con ese valor, los .htaccess se ignoran por completo. La última fila es el ajuste fino que menciona la propia documentación, y me parece el más sensato en un hosting donde solo quieres permitir redirecciones. Las cabeceras como X-Robots-Tag necesitan mod_headers y que el directorio permita Header; las traté en el artículo sobre la cabecera X-Robots-Tag.
Y una limitación obvia: Nginx no lee .htaccess. Si tu hosting usa Nginx delante o en exclusiva, estas reglas no hacen nada y la configuración va en otro sitio.
Redirect y RedirectMatch: lo simple, bien hecho
Para mover una URL a otra, Redirect es lo más legible. Si no pones código, el estado es 302 (la documentación de mod_alias dice que sin argumento de estado la redirección es temporal). Las palabras clave son permanent (301), temp (302), seeother (303) y gone (410), y acepta un número como 308. Este fue el bloque que probé:
Redirect 301 /pagina-vieja /pagina-nueva/
Redirect permanent /servicios-antiguos/ /servicios/
RedirectMatch 301 ^/blog/([0-9]{4})/(.+)$ /articulos/$2
Redirect gone /promo-2019
RedirectMatch 410 ^/tienda/descatalogado/
Redirect /oferta /ofertas/actual/
Redirect 308 /legal /aviso-legal/
| Petición | Respuesta |
|---|---|
/pagina-vieja | 301 a /pagina-nueva/ |
/pagina-vieja?utm_source=x | 301 a /pagina-nueva/?utm_source=x (la query se conserva) |
/pagina-vieja/sub/pagina | 301 a /pagina-nueva//sub/pagina (doble barra) |
/pagina-viejaX | 404: solo coinciden segmentos completos |
/blog/2024/mi-post | 301 a /articulos/mi-post |
/promo-2019 y /tienda/descatalogado/x | 410 |
/oferta (sin código) | 302 |
/legal (código 308) | 308 |
Dos cosas que no salen en los tutoriales. La primera: Redirect trabaja por prefijo y añade el resto de la ruta al destino. Por eso la tercera fila da //: el destino acababa en barra y el resto empezaba con otra. Si vas a mover una carpeta entera con Redirect, escribe origen y destino con el mismo criterio de barras. La segunda: RedirectMatch no está anclado. Sin ^ y $ coincide con cualquier parte de la ruta.
Google trata 301 y 308 como señal fuerte hacia el destino y 302 y 307 como señal débil; el artículo sobre qué tipo de redirección elegir lo desarrolla.
RewriteRule y sus flags
RewriteRule hace lo mismo con más control, y por eso tiene más formas de fallar. La sintaxis es RewriteRule patrón destino [flags], y estos son los flags que uso, con lo que devolvieron:

RewriteEngine On
RewriteRule ^pagina-vieja/?$ /pagina-nueva/ [R=301,L]
RewriteRule ^sin-codigo$ /destino/ [R,L]
RewriteRule ^Mayusculas$ /minusculas/ [NC,R=301,L]
RewriteRule ^buscar$ /resultados/?origen=viejo [R=301,L]
RewriteRule ^buscar-qsa$ /resultados/?origen=viejo [QSA,R=301,L]
RewriteRule ^ancla/(.+)$ /guia/#$1 [NE,R=301,L]
RewriteRule ^ancla-sin-ne/(.+)$ /guia/#$1 [R=301,L]
RewriteRule ^descatalogado - [G,NC]
RewriteRule ^carpeta-vieja/(.*)$ /carpeta-nueva/$1 [R=301,L]
Rsin código es 302. ConR=301es permanente. Un código fuera del rango 3xx, comoR=410, descarta el destino y devuelve ese estado; lo comprobé y dio 410, pero la documentación recomiendaGoFpara esos casos./buscar?q=zapatosacabó en/resultados/?origen=viejo: cuando el destino lleva su propia query, la original se pierde. ConQSAquedó/resultados/?origen=viejo&q=zapatos.NEimporta con anclas: sin él,#seccion-2salió como%23seccion-2en la cabeceraLocation.
L da problemas en .htaccess: lo veremos en los bucles.
El patrón no lleva barra inicial
Este es el error que más veces he visto en reglas copiadas de un VirtualHost a un .htaccess. En contexto de directorio, Apache quita del patrón la ruta del directorio, y lo que queda nunca empieza por barra. La documentación de mod_rewrite lo dice así: un patrón que empieza por ^/ no coincide nunca en este contexto.

# .htaccess
RewriteRule ^vieja$ /nueva [R=301,L] # 301
RewriteRule ^/vieja$ /nueva [R=301,L] # 404, no coincide nunca
# VirtualHost (config del servidor)
RewriteRule ^/vieja$ /nueva [R=301,L] # 301
Si necesitas comparar con la ruta completa dentro de un .htaccess, usa %{REQUEST_URI} en un RewriteCond. Ese es el motivo de que casi todos los bloques de abajo usen ^ a secas como patrón y decidan con condiciones.
Redirigir según la query string
El patrón de RewriteRule solo ve la ruta. Para actuar sobre ?id=42 hace falta un RewriteCond sobre %{QUERY_STRING}, y el flag QSD para que la query vieja no se arrastre al destino:
RewriteEngine On
RewriteCond %{QUERY_STRING} (?:^|&)id=([0-9]+)(?:&|$)
RewriteRule ^producto\.php$ /producto/%1/ [R=301,L,QSD]
RewriteCond %{QUERY_STRING} ^p=([0-9]+)$
RewriteRule ^$ /articulo-%1/ [R=301,L,QSD]
RewriteRule ^sin-parametros$ /destino/? [R=301,L]
| Petición | Respuesta |
|---|---|
/producto.php?id=42 | 301 a /producto/42/ |
/producto.php?color=rojo&id=42 | 301 a /producto/42/ (el parámetro color se pierde) |
/?p=7&x=1 | 200: ^p=([0-9]+)$ no admite más parámetros |
/sin-parametros?a=1&b=2 | 301 a /destino/ (la ? final descarta la query) |
Sin QSD ni ? final, la primera regla devolvió /producto/42/?id=42&color=rojo: la query vieja se pega al destino y se acaba indexando una URL con parámetros que ya no pintan nada. Decide qué parámetros se conservan y cuáles no antes de lanzar la regla, no después.
https, www, barra final e index
Son las cuatro normalizaciones que más se piden, y la mayoría de los tutoriales las resuelve con una regla por cada una. Cada regla extra es un salto extra. Con dos reglas separadas (una para https y otra para www), curl -L contó 2 saltos desde http://ejemplo.test/ruta/pag. Con una sola regla y dos condiciones, 1 salto.

RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\.ejemplo\.test$ [NC]
RewriteRule ^ https://www.ejemplo.test%{REQUEST_URI} [R=301,L]
Resultado, con el servidor escuchando también en TLS:
| Petición | Respuesta |
|---|---|
http://ejemplo.test/ruta/pag?x=1&y=2 | 301 a https://www.ejemplo.test/ruta/pag?x=1&y=2 |
https://ejemplo.test/ruta/pag | 301 a https://www.ejemplo.test/ruta/pag |
https://www.ejemplo.test/ | 200 |
Tres matices que descubrí probando. Uno: no pongas NE en esta regla. Con NE, una petición a /a%20b salió como /a b (espacio sin escapar) en Location; sin NE, Apache la devolvió como /a%20b. Dos: el equivalente de www a no-www es la misma regla con ^www\.(.+)$ y https://%1%{REQUEST_URI}, y también dio 301 con la query intacta. Tres: si el servidor está detrás de un proxy o CDN que termina el TLS, %{HTTPS} vale off siempre y tendrás un bucle; usa la cabecera que envíe el proxy. Con RewriteCond %{HTTP:X-Forwarded-Proto} !https, la petición con X-Forwarded-Proto: https dio 200 y la que no la llevaba, 301.
Qué versión del host dejar como canónica (con o sin www, http o https) es una decisión que va más allá del servidor; la desarrollo en el artículo sobre las versiones de la web con y sin www y http.
Barra final e index
RewriteEngine On
# index.html / index.php -> carpeta (mirando la petición original, no la interna)
RewriteCond %{THE_REQUEST} ^[A-Z]+\s/(.*/)?index\.(?:php|html)[\s?]
RewriteRule ^ /%1 [R=301,L]
# quitar la barra final salvo en carpetas reales
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ /$1 [R=301,L]
| Petición | Respuesta |
|---|---|
/index.html?x=1 | 301 a /?x=1 |
/carpeta/index.html?x=1 | 301 a /carpeta/?x=1 |
/carpeta | 301 a /carpeta/ (lo añade mod_dir, no mi regla) |
/ofertas/ (es un archivo) | 301 a /ofertas |
THE_REQUEST contiene la línea de petición original del cliente y no cambia con las reescrituras internas, así que la regla solo actúa cuando el visitante pidió index.html de verdad. Reconozco un límite: la versión simple, RewriteRule ^(.*/)?index\.html$ /$1 [R=301,L], también funcionó en mi prueba y no dio bucle; uso la condición por precaución, porque con un front controller que reescribe a index.php la petición interna puede volver a coincidir, y eso no lo he reproducido. Para el caso contrario, añadir la barra (RewriteCond %{REQUEST_FILENAME} !-f y RewriteRule ^(.*[^/])$ /$1/ [R=301,L]), respondió 301 a /nueva/ y conservó la query.
Dominio antiguo a dominio nuevo
Un cambio de dominio conserva la ruta y la query y cambia el host. Una regla con condición sobre el host basta para el mapeo URL a URL cuando las rutas no cambian:
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?viejo\.test$ [NC]
RewriteRule ^(.*)$ https://www.nuevo.test/$1 [R=301,L]
http://viejo.test/ruta/pag?x=1 dio 301 a https://www.nuevo.test/ruta/pag?x=1, http://www.viejo.test/ y http://viejo.test/ dieron 301 a la raíz del nuevo, y www.nuevo.test respondió 200. La regla no mapea rutas que cambian ni resuelve las URLs que desaparecen. Ese trabajo (el mapa de redirecciones URL a URL, la preproducción y el seguimiento posterior) es el núcleo de una migración SEO bien planificada, y es donde se gana o se pierde el tráfico.
410 para contenido que no vuelve
Si una URL se retira sin sustituto, no la mandes a la home. Hay dos formas de devolver 410 y las dos dieron el mismo resultado: Redirect gone /promo-2019 (sin destino, porque con estados fuera del rango 3xx el URL debe omitirse) y RewriteRule ^descatalogado - [G,NC]. La respuesta lleva la página de error por defecto de Apache, con título «410 Gone».
¿Es mejor 410 que 404? Google, en su documentación sobre errores HTTP, dice que trata todos los 4xx igual (salvo el 429): una URL indexada que devuelve un 4xx se elimina del índice, y no señala que el 410 sea más rápido. Elijo 410 porque comunica la intención, no porque haya visto que Google reaccione antes. El resto de estados y qué hace cada uno con el rastreo los repaso en el artículo sobre los códigos de respuesta del servidor.
El orden de las reglas
Las reglas de mod_rewrite se ejecutan en el orden en que aparecen, y con [L] la primera que coincide gana. Reproduje dos errores de orden con las mismas dos reglas intercambiadas:

# Mal: la general se come a la específica
RewriteRule ^blog/(.*)$ /articulos/$1 [R=301,L]
RewriteRule ^blog/oferta$ /ofertas/ [R=301,L] # /blog/oferta -> /articulos/oferta
# Bien
RewriteRule ^blog/oferta$ /ofertas/ [R=301,L] # /blog/oferta -> /ofertas/
RewriteRule ^blog/(.*)$ /articulos/$1 [R=301,L]
El segundo caso es el de WordPress y de cualquier web con front controller. Con el bloque típico (si no existe el archivo ni la carpeta, sirve index), una redirección escrita después nunca se evalúa: /viejo-post/ respondió 200 con la página de fallback. Escrita antes, 301 a /nuevo-post/. En WordPress, escribe tus reglas encima del bloque marcado como BEGIN WordPress, porque WordPress regenera lo que hay dentro (esto último no lo probé aquí).
Las cadenas se forman igual: con a a /b y b a /c, pedir /a dio 2 saltos; con a a /c, 1.
Redirect y RewriteRule en el mismo archivo
Para la misma URL puse RewriteRule ^conflicto$ /por-rewrite y Redirect 301 /conflicto /por-alias, en un orden y en el otro. En mi Apache 2.4.58 ganó siempre RewriteRule. No lo he contrastado en otras versiones. En la práctica: no pongas la misma URL en los dos sistemas; elige uno por archivo.
Bucles típicos y cómo salir
Reproduje tres, y los tres los he visto en webs reales.

L en .htaccess reprocesa la petición
RewriteRule ^(.*)$ /app/$1 [L]
Respuesta: 500, y en el error.log, «AH00124: Request exceeded the limit of 10 internal redirects due to probable configuration error». En .htaccess, tras reescribir, Apache vuelve a pasar la petición por las reglas; L solo detiene esa pasada. La documentación recomienda END (desde Apache 2.3.9), que detiene todo el proceso, o una condición. Las dos funcionaron: RewriteRule ^(.*)$ /app/$1 [END] dio 200, y RewriteCond %{REQUEST_URI} !^/app/ delante de la regla con [L], también.
Redirect cuyo destino está dentro del origen
Redirect 301 /blog /blog/articulos/
Como Redirect coincide por prefijo, el destino también empieza por /blog y vuelve a coincidir. curl -L siguió /blog/articulos//articulos/articulos/… hasta cortarlo yo en 4 saltos. Arreglo: RedirectMatch 301 ^/blog/?$ /blog/articulos/, que dio un único salto y un 200.
Reglas de www que se contradicen
Una regla del panel del hosting que añade www y otra, heredada, que lo quita: cada una devuelve 301 a la otra. Con curl -L --max-redirs 6 acabó en 6 saltos y con el error 47 de curl (demasiadas redirecciones). Los navegadores acaban mostrando un error de demasiadas redirecciones, y Google, que según su documentación sigue hasta 10 saltos, no llega al contenido. La solución es una sola regla canónica, como la del apartado anterior, y borrar la otra.
Rendimiento: por qué .htaccess no es el mejor sitio
La documentación de Apache es clara: si tienes acceso al archivo de configuración principal, pon allí las reglas, también las de mod_rewrite, porque se cargan una vez al arrancar y no en cada petición. Con AllowOverride activado, Apache busca .htaccess en cada directorio de la ruta, existan o no, y lo lee cada vez que se pide un documento.

Medí el coste en mi contenedor, con un archivo estático en una ruta de varios niveles. Sin .htaccess y con AllowOverride None: unas 22.900 peticiones por segundo. Con un .htaccess vacío y AllowOverride All: unas 19.900. Con 200 RewriteRule que no coinciden en .htaccess: unas 4.400. Las mismas 200 reglas dentro de un <Directory> de la configuración: unas 19.600. No es un benchmark de tu servidor: te dice la proporción, no el valor. Con pocas reglas el efecto es pequeño; con cientos, el coste de leerlas pasa a dominar la petición de un archivo estático.
Para las redirecciones de verdad masivas hay otra limitación: RewriteMap no se puede declarar en .htaccess, solo en la configuración del servidor. Si tienes miles de URLs que mapear, eso ya no es un trabajo de .htaccess.
La misma lógica en un VirtualHost la probé en un puerto aparte y funcionó con la barra inicial en el patrón: RewriteRule ^/vieja$ /nueva [R=301,L] dio 301, y Redirect permanent /legado /moderno también. El artículo sobre las redirecciones en la configuración de Apache desarrolla ese camino.
Cómo depurar y verificar
Mi orden, de lo más barato a lo más ruidoso:

curl -I(y-L). Estado yLocationde cada salto, y el contador%{num_redirects}para detectar cadenas. Prueba conR(302) y cambia aR=301al final: Apache recomienda 302 mientras pruebas porque los navegadores cachean las permanentes.- El error.log. Un
.htaccesscon un error de sintaxis devuelve 500 en todas las URLs del directorio y deja la causa aquí: conRewriteRulescrito mal salió «Invalid command 'RewriteRul', perhaps misspelled…»; conRewriteRule ^a$sin destino, «RewriteRule: bad argument line '^a$'». - Traza de mod_rewrite.
LogLevel alert rewrite:trace3en la configuración del servidor. Si lo pones en.htaccess, da 500 con «LogLevel not allowed here». La documentación avisa de que por encima detrace2el servidor se ralentiza mucho: actívalo para el caso y quítalo. apachectl configtest. No revisa los.htaccess: me devolvió «Syntax OK» con un.htaccessroto que provocaba 500. No es una garantía de que las reglas estén bien.
Esto es lo que dejó la traza de una petición a http://ejemplo.test/ruta/pag?x=1 (recortadas las partes comunes de cada línea):
[perdir /srv/ht-redir/doc/] strip per-dir prefix: /srv/ht-redir/doc/ruta/pag -> ruta/pag
[perdir /srv/ht-redir/doc/] applying pattern '^' to uri 'ruta/pag'
[perdir /srv/ht-redir/doc/] rewrite 'ruta/pag' -> 'https://www.ejemplo.test/ruta/pag'
[perdir /srv/ht-redir/doc/] copying x=1 to query string for redirect
[perdir /srv/ht-redir/doc/] redirect to https://www.ejemplo.test/ruta/pag?x=1 [REDIRECT/301]
La línea strip per-dir prefix es la que explica por qué el patrón no lleva barra inicial. Con trace3 no vi las líneas de cada RewriteCond; no he probado niveles más altos.
Auditar una lista de URLs
Después de cualquier cambio, comprueba la lista de URLs antiguas que importan (las de Search Console, las del sitemap viejo, las que reciben enlaces). Con un archivo urls.txt:
while read -r u; do
curl -s -o /dev/null -L -w "%{http_code} saltos=%{num_redirects} %{url_effective}\n" "$u"
done < urls.txt
Busca tres cosas: todo acaba en 200 (o 410 si lo decidiste), nada tiene más de un salto y nada termina en una URL con parámetros que no tocaban.
Cómo lo tengo en mi web
El .htaccess de cristofercruz.net tiene 33 líneas y ninguna redirección. Lo que contiene:
DirectoryIndex index.phpyOptions -Indexes.ErrorDocument 404 /404.phpyErrorDocument 403 /404.php; el README indica que404.phpdevuelve estado 404 connoindex.- Con
mod_headers:X-Content-Type-Options,Referrer-Policy,X-Frame-Optionsy las reglas deCache-Control(siete días para estáticos, un año conimmutablepara los que llevan hash de 12 caracteres). La lógica de esa política la cuento en el artículo sobre la caché y el SEO. mod_deflatepara HTML, CSS, JS, JSON, XML y SVG.- Un
FilesMatchconRequire all deniedpara documentación interna y archivos de vista previa, y otro para.gityDESPLIEGUE.md. RewriteEngine Oncon dos reglas[F,L]que prohíbentools/y.git/.
No hay redirección de http a https ni de www a la versión sin www. El README lo deja anotado en «Pendiente de verificar en el hosting: redirecciones https/no-www (no se han tocado por depender del servidor)». La URL canónica que declara el sitio en config.php es https://cristofercruz.net, sin www. No he comprobado desde aquí qué responde el servidor de producción; puede que el hosting ya lo resuelva, y es lo primero que miraría con curl -I.
Si hubiera que hacerlo en este .htaccess, pondría el bloque de una sola regla justo después de RewriteEngine On y antes de las reglas que prohíben carpetas. Lo probé sobre una copia del archivo real con la versión sin www:
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^cristofercruz\.net$ [NC]
RewriteRule ^ https://cristofercruz.net%{REQUEST_URI} [R=301,L]
http://cristofercruz.net/robots.txt?x=1 dio 301 a https://cristofercruz.net/robots.txt?x=1; http://www.cristofercruz.net/tools/a.txt dio 301 a https://cristofercruz.net/tools/a.txt (con el dominio de prueba, la regla de tools/ siguió dando 403 tras el salto); https://cristofercruz.net/robots.txt, 200. Un efecto secundario que conviene saber: esa condición de host redirige también cualquier otro nombre, y en mi prueba una petición a 127.0.0.1 acabó en 301 hacia producción. Si tienes un entorno de desarrollo o pruebas en otro host, añade una condición para excluirlo. Y en las carpetas, /blog sin barra da dos saltos (el de la canónica y el de mod_dir añadiendo la barra), así que las URLs del blog llevan barra final.
Antes de tocar un .htaccess de producción, guarda una copia y prueba con R (302) y curl. Un error de sintaxis en un .htaccess devuelve 500 en todas las URLs del directorio, y un navegador que ya guardó una 301 equivocada la sigue aplicando aunque corrijas la regla.
Preguntas frecuentes
¿Redirect o RewriteRule?
Redirect (o RedirectMatch) cuando la regla solo depende de la ruta: mover una página o una carpeta. RewriteRule cuando depende del host, de https o de la query string, o cuando necesitas flags como QSD o NE. Mezclarlos para la misma URL no es buena idea: en mi prueba ganó RewriteRule en cualquier orden.
¿Por qué mi .htaccess no hace nada?
Lo más probable es que el directorio tenga AllowOverride None, que ignora el archivo sin dar error, o que el servidor sea Nginx, que no lo lee. Prueba a escribir una línea inválida: si la web no devuelve 500, Apache no está leyendo ese archivo.
¿Cuánto tarda Google en reflejar una 301?
No está documentado. Google explica que una redirección permanente indica que el destino debe ser el canónico y que, en un cambio de dominio, las URLs viejas pueden seguir apareciendo durante un tiempo, pero no da plazos. No cuentes con un número.
¿Cuántas redirecciones aguanta un .htaccess?
No hay un límite documentado, pero cada regla se evalúa en cada petición y el archivo se vuelve a leer cada vez. En mi prueba, 200 reglas bajaron el rendimiento de unas 19.600 a unas 4.400 peticiones por segundo. Con cientos o miles de URLs, es mejor la configuración del servidor, con RewriteMap.
¿Uso 301 o 302 mientras pruebo?
302 (R sin código). La documentación de Apache lo recomienda porque los navegadores cachean las redirecciones permanentes, y una regla mal escrita se queda pegada aunque la corrijas. Cuando el resultado sea el correcto, cambia a R=301.
¿410 o 404 para una página eliminada?
Para Google son equivalentes: su documentación dice que todos los 4xx, salvo el 429, se tratan igual y que una URL indexada que devuelve un 4xx se elimina del índice. Uso 410 cuando la retirada es definitiva y 404 cuando no estoy seguro.
Referencias y fuentes
Documentación consultada para este artículo.
- Apache HTTP Server Tutorial: .htaccess files Apache HTTP Server
- Apache Module mod_alias Apache HTTP Server
- Apache Module mod_rewrite Apache HTTP Server
- Apache mod_rewrite: RewriteRule Flags Apache HTTP Server
- Apache mod_rewrite: Rewriting in .htaccess Apache HTTP Server
- Apache mod_rewrite: When not to use mod_rewrite Apache HTTP Server
- Redirects and Google Search Google Search Central
- HTTP status codes, and network and DNS errors Google Search Central




