Developer Tools

htaccess Explainer: Reading the File Nobody Reads

Every WordPress site on Apache has an .htaccess, and Apache reads it on every request for every directory in the path. Here is what the WordPress block actually does, why [L] does not mean last, the difference between Redirect and RewriteRule, and which pasted security snippets have done nothing since Apache 2.4.

htaccess Explainer: Reading the File Nobody Reads

Open the document root of any WordPress site running on Apache and there is a file called .htaccess in it. No extension, hidden by default in most FTP clients, and the only file on the server that can take the entire site down with one bad line. Almost nobody who owns one has read it.

What is usually in there is sediment. WordPress writes nine directives into it the first time you save a permalink structure. A cache plugin adds thirty lines of expiry headers. A security plugin adds a chunk labelled with its own name. Somebody pasted six rules from a forum thread in 2019 to harden the site, and nobody has touched them since. Then the host moved you from mod_php to PHP-FPM, one php_value line stopped being legal, and every URL started returning a 500.

The file gives you no help while any of this is happening. Apache has two responses to a directive it dislikes: ignore it in silence, or refuse the whole request with a 500. There is no warning level in between, and nothing anywhere tells you that a rule you wrote can never be reached.

What the file actually is

Apache’s real configuration lives in files only the host can edit. An access file hands a slice of that configuration to whoever can write into a directory, and the size of the slice is set by AllowOverride in the virtual host: FileInfo covers the rewrite engine, Redirect, Header and ErrorDocument, Limit covers the old access directives, AuthConfig covers Require and password protection, Options covers Options and the PHP module settings. A directive outside your slice is not ignored, it is a 500.

Below is the reading aid. Paste your file in, and every line gets a sentence beside it, generated blocks get named and folded, and the problems are listed worst first. Two switches change the verdicts, because the same line is fine or fatal depending on the server: PHP as an Apache module or through FPM, and Apache 2.4 or 2.2. It all happens in your browser. There is no upload, and the file never leaves the page.

htaccess explainer

Paste an .htaccess you inherited and read it back in plain sentences. Every directive gets a line of English beside it, the blocks a plugin generated are named and folded away, the mistakes are listed worst first, and you can walk a real URL through the file to see which line acts on it and in what order. Everything is worked out in this browser tab, nothing is uploaded and nothing is fetched.

A line ending in a backslash is joined with the next one, quoted arguments are kept whole, and the first 1200 lines are read.

How this server runs

How the file reads

A block between BEGIN and END markers belongs to whatever wrote it, and is replaced wholesale the next time that plugin saves. Fixing anything inside one of those lasts until then.

What is wrong with it, worst first
    Walk a URL through the file
    WunderPaint
    The Dynamic Design and Automation Studio
    WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

    The rest of this is htaccess explained the way the server reads it: top to bottom, twice, with no commentary on anything it skipped.

    The price of a file read on every request

    Apache does not read the file once at startup, and it does not read only the one in the document root. For a request to /wp-content/uploads/2026/05/photo.jpg it looks for an access file in every directory along the path, from the filesystem root down to the folder holding the file. Each of those is a filesystem lookup, each file found is parsed from scratch, and nothing is cached between requests, because the point of the mechanism is that an edit takes effect on the next hit without anybody restarting anything.

    Table of the eight directories Apache searches for an access file when serving one image from the uploads folder, with only the document root holding a file, plus three figures: eight lookups per request, zero parses reused, one parse at startup if the same rules live in the virtual host.

    The same rules in the virtual host are parsed once when Apache starts and then cost nothing. That is why Apache’s own documentation tells you to avoid access files whenever you can edit the server configuration, and why a good number of managed hosts either set AllowOverride None (in which case Apache does not even look for the file) or run nginx, which has no equivalent at all. LiteSpeed does read it, which is why a LiteSpeed cache plugin writes to it happily.

    None of this makes an access file the reason your site feels slow. A few stat calls are nothing next to an uncached query or a 4 MB hero image, and the order you investigate a slow site in matters more than any single fact, which is what the diagnostic order for a slow WordPress site is about. It does mean that 300 lines of pasted snippets are pure overhead, paid on every image, every stylesheet and every page view.

    The WordPress block is a fallback, not a router

    Here is what WordPress writes, unchanged for years, on a single site install with pretty permalinks:

    # BEGIN WordPress
    # The directives (lines) between "BEGIN WordPress" and "END WordPress" are
    # dynamically generated, and should only be modified via WordPress filters.
    # Any changes to the directives between these markers will be overwritten.
    <IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    RewriteBase /
    RewriteRule ^index.php$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
    </IfModule>
    # END WordPress

    The IfModule wrapper stops the file killing a server that has no mod_rewrite. The E=HTTP_AUTHORIZATION line copies the Authorization header into an environment variable, because under CGI or FPM it does not otherwise reach PHP, and application passwords on the REST API depend on it. RewriteBase supplies the prefix for any substitution written as a relative path. The ^index.php$ rule stops the front controller testing itself.

    Then the three lines that do the work: if the request does not name a real file, and does not name a real directory, hand it to index.php. That is the entire contribution of Apache to WordPress routing. Which post loads, which archive, which taxonomy term, all of that happens in PHP against REQUEST_URI, using the rewrite rules stored in the database that get rebuilt when you flush permalinks. Those rules never appear in this file.

    That split is worth holding onto. A permalink returning 404 while the rest of the site works is a PHP routing problem, not an access file problem. A missing image is the opposite: the front controller only fires when the file is not on disk, so a broken image is a question about the file, the path and the permissions first, which the five layers behind images that will not show takes in order.

    Everything between the two markers is disposable. save_mod_rewrite_rules() in wp-admin/includes/misc.php calls insert_with_markers(), which keeps every line before # BEGIN WordPress and every line after # END WordPress and replaces the middle wholesale. A rule you add inside survives until the next time somebody presses save on the permalink settings. Put your rules above the block, or use the mod_rewrite_rules filter, which is the supported way to change what gets written. On multisite the function bails out before writing anything, which is why network installs print the rules on screen and expect you to paste them in.

    The flags, and the second pass

    [L] is the flag everyone thinks they understand. In the server configuration it does mean last: rewriting stops. In an access file it is narrower. Rewriting stops for this pass, and then, if the URL actually changed, Apache hands it back to the URL mapping machinery, walks the directories again and runs the whole file again from the top. The catch-all in the WordPress block does not end the request. It restarts it.

    Flow diagram of a request for /hello-world/ through a WordPress access file: the conditions match, the catch-all rewrites to index.php with the L flag, the request restarts and runs the file again from the top, rules below the block only ever see index.php, and PHP does the routing. A code panel contrasts the L flag with END.

    On that second pass the request is /index.php, a real file, so the conditions fail and the rule does not fire again. Everything below the block still runs, but it now sees index.php rather than the path the visitor typed. That is why a redirect added at the bottom of the file appears to be ignored: it is not ignored, it just never sees the URL it was written for. One line quietly eating everything below it is the most common thing an inherited file has wrong with it.

    [END], added in Apache 2.3.9, is the flag that means what people expect [L] to mean: stop rewriting, start no further pass. If the passes never settle, LimitInternalRecursion stops them at ten and Apache returns a 500. That is what a rewrite loop looks like from the outside.

    The other trap has nothing to do with flags. A RewriteCond binds to the next RewriteRule and nothing else. Two conditions and two rules underneath them are not a group of four: the conditions apply to the first rule, and the second rule is unconditional. Files written by hand are full of this, usually as two rules that look like a matched pair because they were copied one from the other.

    Redirect and RewriteRule are not two spellings of the same thing

    Redirect and RedirectMatch come from mod_alias, an older and much simpler module. Redirect matches a prefix and appends whatever followed it, so Redirect 301 /blog /news sends /blog/hello/ to /news/hello/. RedirectMatch takes a regular expression and appends nothing, so anything carried over has to be captured and written into the target. Both always produce a redirect the browser can see, and both need a target that is a full URL or a path starting with a slash. A relative target produces a Location header the browser resolves in ways you did not intend.

    RewriteRule does all of that and more: [R=301] for an external redirect, no flag at all for an internal rewrite the visitor never sees, [F] for a 403, [G] for a 410, [QSA] to keep the query string. It can also test what Redirect cannot see, which is what RewriteCond is for: the query string, the host, the method, a header.

    Mixing the two is where it gets confusing, because the file is not evaluated in the order it is written. Inside an access file the rewrite engine gets the request before mod_alias does, whatever the line numbers say. Put a Redirect above the WordPress block and the front controller still rewrites first, the request restarts as /index.php, and your redirect is compared against that. On a WordPress site, write redirects as RewriteRule with [R=301,L] above the block. While you are testing use 302, because browsers cache a 301 hard and you will spend an afternoon debugging your own cache.

    The snippets that stopped doing anything

    Most inherited files carry a security section pasted from a forum. Some of it earns its place. A good deal of it has been decorative for a decade.

    • Options -Indexes works. Plenty of default configurations still switch directory listings on for the document root. Keep this one.
    • Protecting .htaccess from being served is redundant. Stock Apache configuration already denies any file whose name starts with .ht. That block is a second lock on a locked door.
    • <Limit GET POST> means the opposite of what people think. Limit restricts only the methods you name. Everything you did not name stays unrestricted. The directive that means “everything except these” is LimitExcept.
    • Order allow,deny is Apache 2.2 syntax. On 2.4 it survives only while the compatibility module is loaded; without it the line is a fatal unknown command. Mixing those directives with 2.4’s Require in one context is not supported, and the result is not what either half promises.
    • Header set X-XSS-Protection "1; mode=block" talks to browsers that no longer exist. Chrome removed its XSS auditor in 2019, Edge followed, Firefox never shipped one.
    • Header set is not Header always set. Plain set only applies to successful responses, so your security headers quietly vanish from every 404 and every 500. always is the one that covers error responses too.
    • User agent blocklists are theatre. The user agent is a string the client chooses. Referer rules against hotlinking do work on ordinary browsers, but they are easy to write in a way that also blocks Google Images, which stopping hotlinking without breaking image search goes through.
    • php_value and php_flag only exist under mod_php. Under FPM or CGI they are an unknown command and every request returns 500. Those settings belong in php.ini, a .user.ini, or in the constants that earn their place in wp-config.php.

    If you want one header in that section to be worth the bytes, make it Content-Security-Policy, set with Header always set. It is the one that actually stops a class of attack rather than announcing an intention, and it is also the one most likely to break your own site if you write it blind, which is the subject of the header nobody dares switch on.

    Whether your server reads the file at all

    Before you debug a single rule, find out whether anything is reading them. The test takes ten seconds and no shell access: put one line of nonsense at the top of the file, something like ThisIsNotADirective, save, and load the site. A 500 on every URL means the file is read and parsed, and you can take the line out again. A site that carries on as before means it is not, and every rule in there has been decoration.

    Two things can muddy the result. AllowOverride may be set to a narrow list, so nonsense is fatal while the directives you actually care about are still refused one by one, and Apache 2.4 has a Nonfatal option that downgrades exactly this error to a log line. If you can read the error log, that is where the truth is: a rejected directive is logged with the file path and the line number. On nginx the answer is permanent, because the file is never read at all, and plenty of WordPress sites carry an .htaccess that has done nothing for years.

    What the explainer deliberately does not do

    The tool reads the first 1200 lines and says so if there are more. Patterns over 400 characters, and patterns JavaScript cannot compile, are stepped over rather than guessed at. Every module named in the file is treated as loaded, so a negated IfModule is skipped. The method is GET. Filesystem tests come from the checkbox marked as a real file on disk rather than from a disk, and that checkbox is the whole game for a front controller: off, and the request falls through to index.php; on, and the file is served without any rewriting.

    A condition testing something no browser tab can know, a user agent or a cookie, is taken as matching and the hop says so. mod_alias redirects are walked in file order with a note that the real server does not run them there. Six internal passes and eight external hops, then it calls the result a loop.

    What it is not is Apache. It does not repair, rewrite or generate a file, and it will not tell you what a specific build with a specific set of loaded modules is going to do at three in the morning. When you need that level of certainty and you have access to the virtual host, turn on the rewrite log with LogLevel alert rewrite:trace3, make one request, and read the engine’s own account of every rule it tried. Then turn it off again, because it is loud.

    A file worth reading once

    The honest summary of .htaccess is that it is a workaround with good manners. It exists because shared hosting needed a way to let customers change configuration without letting them into the configuration, and it pays for that with a filesystem walk and a parse on every request, forever. Anything in it that could live in the server configuration should, and where you cannot do that, keep the file short enough to read in one screen.

    Most of the damage in an inherited file is not a wrong rule. It is a right rule in the wrong place: below the front controller, after an unconditional catch-all, or attached to a condition that binds to a different line than the author assumed. Those mistakes are invisible in the file and in the browser, because Apache never reports what it decided not to do. Walking one real URL through the file usually teaches more than reading it does.

    So spend twenty minutes on it once. Read the findings, delete the blocks left by plugins you removed two years ago, move anything you wrote out of the WordPress markers, and check that the header you thought was protecting the site is set with always. Then leave a comment at the top with the date and your initials, so the next person to open the file, probably you, can tell which parts were on purpose.

    htaccess Explainer: Reading the File Nobody Reads

    Table of Contents

    Learn it by building something

    Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

    One mail a week, and then it ends.
    Unsubscribe in one click.

    Troubleshooting

    The WordPress White Screen of Death Explained

    A white screen with no error message isn't a WordPress bug. It's PHP dying silently mid-request. Here's how to see the real error in minutes and the exact order to check plugins, theme, and memory limits.

    Speed & Performance

    WordPress Lazy Loading Images: What Core Does and When It Hurts

    WordPress has added loading="lazy" itself since 5.5, and it skips the first three media elements on purpose, because the top one is usually the largest contentful paint element. Here is the real mechanism: the functions, the threshold of 3, fetchpriority, and the places where the logic never runs.

    Photo Editing

    Repair a photo that messengers and re-saves have turned to blocks

    JPEG damage is not random. It is made in blocks of eight by eight pixels, in a known order, which is why a model trained on compressed images beats any amount of sharpening. It runs in your browser, repairs at four times the size and hands the result back clean.

    Troubleshooting

    Why Your WordPress Images Look Blurry, and Which Cause It Actually Is

    Blurry WordPress images are not one problem but six, and each leaves a different fingerprint. Work out which symptom you have before you change anything, because most of the fixes do nothing for most of the causes.

    Photo Editing

    Take the scratches and dust out of a scanned family photograph

    A scratch is thin and disagrees with its surroundings in almost every direction at once. A real edge disagrees in one or two. That single difference finds the damage with plain arithmetic, and an inpainting model fills what you agree to. All of it in your browser.

    WordPress Images

    WooCommerce Product Image Size: The Three Settings That Decide Everything

    WooCommerce shows the same photograph in three places using three different files. Here are the three registered sizes, the cropping option that decides what happens to a mixed range, and why changing a width appears to do nothing.

    Download the free WunderPaint Plugin for WordPress

    The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

    The Image Editor & Design Studio

    Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

    Free

    Chaos Art

    Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

    Pro

    Particle Strokes

    Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

    Pro

    City Diorama

    Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

    Free

    Papercut Art

    Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

    Pro

    3D Earth Studio

    A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

    Free

    Mystic Studio

    Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

    Free

    Marble Bath

    Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

    Free

    Day Ring

    Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

    Free

    Code Shot

    Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

    Pro

    3D Solar System Studio

    Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

    Pro

    3D Molecule Studio

    Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

    Pro

    3D Textile Studio

    Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

    Pro

    3D Particle Studio

    Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.