Apache .htaccess Practical Guide: When I Use It, When I Avoid It, and What Usually Goes Wrong

.htaccess is one of those Apache features that refuses to die because it solves a real problem, especially on shared hosting and delegated environments. At the same time, it is responsible for a lot of messy rewrite rules, unnecessary per-request filesystem checks, and 500 errors caused by one bad line buried in a content directory.

I do not hate .htaccess. I just do not reach for it first on servers I control. This guide explains what .htaccess can and cannot do, how AllowOverride changes the picture, which use cases are still reasonable, and when I move the logic into the main Apache configuration instead. If you need the bigger picture first, read my Apache VirtualHost configuration guide and my mod_rewrite and HTTPS enforcement article. For TLS and full-site setup, my Apache SSL best practices guide is the better place to start.

What .htaccess is actually for

A .htaccess file is a per-directory Apache configuration file. Apache checks for it while processing requests if overrides are allowed for that directory tree.

That means a site owner without access to the main server config can still define things like:

  • redirects
  • rewrite rules
  • basic authentication
  • custom error documents
  • MIME type changes
  • caching or compression headers

This is why .htaccess is still common on shared hosting. It gives tenants some control without handing them the main Apache config.

On dedicated servers or well-managed VPS deployments, I prefer putting the same logic in the main VirtualHost or directory config whenever possible.

The performance cost of .htaccess

This is the biggest reason I avoid it on systems I administer directly.

When .htaccess is enabled, Apache may check each directory in the request path for an override file on every request. That filesystem overhead is not catastrophic on tiny sites, but it is still unnecessary work when you could define the directives once in the main config.

That is why AllowOverride None is the usual performance-friendly default:

<Directory /var/www/example.com/public>
    AllowOverride None
    Require all granted
</Directory>

If I control the server and deploy process, that is what I prefer. Central config is faster, easier to audit, and easier to test before reload.

AllowOverride and what it really means

.htaccess only works where AllowOverride permits it.

Common values include:

  • None — ignore .htaccess completely.
  • All — allow all override classes.
  • FileInfo — allow file handling, rewrites, MIME types, and related directives.
  • AuthConfig — allow authentication directives.
  • Limit — allow access control and method restrictions.

Example:

<Directory /var/www/example.com/public>
    AllowOverride FileInfo AuthConfig
    Require all granted
</Directory>

I try hard not to use AllowOverride All unless I truly have to. It is broad, and broad usually means harder to reason about later.

Also note the negative case: if .htaccess changes are being ignored, AllowOverride is one of the first things I check.

A simple redirect in .htaccess

For straightforward redirects, Redirect from mod_alias is often enough.

Redirect 301 /old-page https://example.com/new-page

That is cleaner than dragging mod_rewrite into the job when you just need one path moved.

A pattern-based version uses RedirectMatch:

RedirectMatch 301 ^/legacy/(.*)$ https://example.com/archive/$1

I prefer these for simple cases because they are easier to read and less likely to create loops than overly clever rewrite rules.

Using mod_rewrite in .htaccess

Sometimes you really do need rewrite logic. Then a typical .htaccess file starts like this:

RewriteEngine On

RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

That enforces HTTPS for the current host. It is valid, but there are two practical cautions.

First, if you control the VirtualHost config, I would rather do this redirect there. It is clearer and more efficient.

Second, rewrite rules in .htaccess are sensitive to path context. People copy rules from vhost examples and forget that the leading slash behavior differs.

RewriteBase and path confusion

RewriteBase is one of those directives that used to show up everywhere because examples were copied blindly.

Sometimes it is necessary, especially in subdirectory applications. Often it is not. A lot of broken .htaccess files contain an unnecessary or wrong RewriteBase that sends requests somewhere bizarre.

Example of a subdirectory app:

RewriteEngine On
RewriteBase /app/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /app/index.php [L]

If the application lives at the document root, that same rule set would likely need different paths. I always test rewrites with real URLs instead of assuming a borrowed snippet fits.

Basic authentication in .htaccess

One of the genuinely useful .htaccess jobs is quick Basic Auth protection for a directory.

AuthType Basic
AuthName "Restricted Area"
AuthUserFile /etc/apache2/.htpasswd-example
Require valid-user

Create the password file like this:

sudo htpasswd -c /etc/apache2/.htpasswd-example admin

I do not store the password file inside the document root. Keep it outside web-accessible paths.

This pattern is handy for staging sites, internal dashboards, or maintenance access. For a fuller walkthrough, I covered the Nginx equivalent in my Nginx basic auth guide, but the same operational caution applies: Basic Auth should ride over HTTPS, not plain HTTP.

Custom error pages

.htaccess can point to custom error documents.

ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html

This is useful, but I keep the error pages simple. If your custom 500 page depends on the same broken application stack that caused the original error, you can end up with confusing fallback behavior.

Also verify the paths actually resolve:

curl -I http://example.com/does-not-exist

Compression and caching headers

.htaccess can also manage browser-facing performance headers when AllowOverride FileInfo is permitted.

Compression example with mod_deflate:

<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
</IfModule>

Caching headers example with mod_expires:

<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType text/css "access plus 7 days"
    ExpiresByType application/javascript "access plus 7 days"
    ExpiresByType image/png "access plus 30 days"
</IfModule>

These are fine, but again, on servers I control, I would rather place them in the main config so I can validate them centrally.

MIME type overrides

Sometimes an application needs an explicit MIME mapping.

AddType application/wasm .wasm
AddType text/plain .log

That is a reasonable .htaccess use when the hoster allows limited overrides and you do not control the vhost. Just be careful not to create odd security behavior by forcing downloads or browser execution for the wrong file types.

Options in .htaccess

Options can be useful and dangerous.

Common examples:

Options -Indexes
Options +FollowSymLinks

Options -Indexes prevents directory listings when no index file exists. I usually want that.

FollowSymLinks is common, but in .htaccess usage it depends on server policy and directory permissions. Some hosts restrict it.

I avoid spraying Options directives around without checking whether they are actually allowed. Unsupported or disallowed options can trigger server errors.

How parent and child .htaccess files interact

Another detail people forget is that .htaccess behavior can stack through the directory tree. A request for /app/admin/report.html may be affected by rules in the document root, /app/, and /app/admin/ depending on layout and AllowOverride scope. That can be useful, but it also means a local change might inherit behavior from a parent directory that nobody remembered existed.

When a rule behaves strangely, I check for multiple .htaccess files first:

find /var/www/example.com -name .htaccess -print

If there are several, I read them in directory order and map which request path traverses which files. I have seen redirect loops caused by a root-level canonical host rule combined with a lower directory rule that tried to force a subdirectory path. Both rules were individually reasonable. Together they were a mess.

Module availability matters more than the snippet

A valid-looking .htaccess file still fails if the required Apache modules are not loaded. That is obvious in theory and easy to forget in practice, especially on minimal installs or after migrations.

A few quick checks:

sudo apache2ctl -M | grep rewrite
sudo apache2ctl -M | grep headers
sudo apache2ctl -M | grep expires

If the module is missing, the directive may throw a server error or quietly never do what you expected. I prefer confirming module state before I spend time debugging rule syntax.

PHP directives in .htaccess: old habit, limited usefulness now

Older Apache and mod_php setups often used .htaccess for PHP settings such as:

php_value upload_max_filesize 64M
php_value post_max_size 64M

That can still work in environments using mod_php, but it does not apply the same way to PHP-FPM behind proxy_fcgi on many modern stacks. In those setups, people paste old php_value examples into .htaccess, get a 500 error, and assume Apache is broken.

Current practice is to set PHP-FPM pool values or use the application’s supported configuration method instead of assuming .htaccess controls PHP runtime everywhere. If you are on Nginx plus PHP-FPM, that advice is doubly outdated, and my PHP-FPM and Nginx tuning guide covers the stack more directly.

A safer testing workflow for rewrite changes

I try hard not to edit .htaccess live and hope for the best. My preferred workflow is:

  1. copy the current file to a backup in the same directory
  2. make one logical change at a time
  3. test with curl -I for the exact affected URLs
  4. watch the error log while testing

Example backup and test sequence:

cp .htaccess .htaccess.bak
curl -I http://example.com/old-page
curl -I http://example.com/app/

That is deliberately simple. If you change redirects, auth, and caching rules at once, debugging the failure later is harder than it needs to be.

Common .htaccess mistakes I keep seeing

Infinite redirect loops

Classic example: enforcing HTTPS in .htaccess while Apache is behind a reverse proxy and the backend connection is plain HTTP. Apache thinks every request is insecure and keeps redirecting.

If you are behind a proxy, you may need to trust X-Forwarded-Proto at the vhost layer instead of using a naive .htaccess rule.

Wrong rule context

Rules copied from VirtualHost config do not always work unchanged in .htaccess because path matching differs.

Bad RewriteBase

Especially common in CMS migrations and subdirectory apps.

Syntax errors causing 500 responses

One typo in .htaccess can take out an entire directory tree.

AllowOverride None

The file exists, looks correct, and does absolutely nothing because overrides are disabled.

Debugging .htaccess problems on Apache 2.4

On Apache 2.4, the old RewriteLog directive from Apache 2.2 is deprecated and should not be used in current configs. Modern debugging is done with LogLevel.

For rewrite tracing:

LogLevel warn rewrite:trace3

Increase the trace level temporarily when needed, but do not leave extremely verbose rewrite tracing enabled on busy systems.

Then review the error log:

sudo tail -f /var/log/apache2/error.log

You will see how rewrite conditions and rules are being evaluated. This is much better than staring at a rule and guessing.

Old Apache 2.2 advice to treat carefully

You will still find old .htaccess guides recommending RewriteLog, broad Order allow,deny syntax, or other Apache 2.2-era patterns. Treat those as historical, not current best practice.

On Apache 2.4, current access control looks like this:

Require all granted

not the old 2.2 style.

If you inherit a mixed-era config, clean it up deliberately rather than adding one more legacy snippet on top.

When I do use .htaccess

I still consider .htaccess reasonable in a few cases:

  • shared hosting where vhost access is unavailable
  • delegated application ownership where site admins manage their own redirects
  • temporary maintenance changes that must be applied inside content management constraints
  • packaged applications that officially expect certain overrides

Even then, I try to keep the file short and purposeful.

When I do not use .htaccess

If I have root or normal config management access, I prefer the main Apache config for almost everything:

  • redirects
  • auth rules
  • directory options
  • MIME types
  • compression
  • cache headers
  • application rewrites

That keeps behavior centralized and avoids per-request directory scanning.

Validation steps I actually run

Whenever I change the main config that affects .htaccess processing, I validate first:

sudo apache2ctl -t
sudo apache2ctl -S

Then I test real responses:

curl -I http://example.com/old-page
curl -I http://example.com/protected/
curl -I http://example.com/missing-page

For auth directories, I also test with credentials:

curl -I -u admin:yourpassword http://example.com/protected/

And of course I inspect the error log if a 500 appears.

A simple pattern for hiding sensitive files

One practical .htaccess job on shared hosting is denying access to files that should never be served directly. A small example looks like this:

<FilesMatch "^(\.env|composer\.(json|lock)|package\.json)$">
    Require all denied
</FilesMatch>

I treat this as defense in depth, not an excuse to place secrets carelessly inside the document root. Sensitive files should ideally live outside the served tree altogether. Still, on constrained hosting or legacy apps, a deny rule like this can prevent an embarrassing leak caused by a bad deploy or misrouted request.

After adding a rule, I test it explicitly with curl -I against the paths I expect to be blocked. A security rule that was never verified is just optimism in configuration form.

Security considerations

.htaccess can protect resources, but it can also weaken security if used casually.

I watch for:

  • password files stored inside web roots
  • overly broad AllowOverride All
  • rewrite rules that expose internal files
  • misconfigured directory options leading to listings
  • auth over plain HTTP instead of HTTPS

I also make sure hidden files themselves are not exposed. Many setups deny direct access to .ht* files in the main config for exactly that reason.

Final thoughts

.htaccess is useful when server access is limited and local control is necessary. Outside that situation, I treat it as a compatibility tool, not my first choice. Main Apache configuration is faster, easier to validate, and easier to understand during incidents.

My practical rule is simple: if I control the server, I put the logic in the VirtualHost or directory config. If I do not control the server, I keep .htaccess minimal, test every rewrite carefully, and assume that one careless line can turn into a 500 at the worst possible moment. That mindset has saved me from more than one self-inflicted outage, and it keeps emergency fixes from turning into permanent technical debt over time consistently.

Scroll to Top