You did everything right. You added hidden fields to your contact form, wrote the JavaScript to pull UTM parameters from the URL, tested the whole setup in your browser, and watched the values flow through perfectly. utm_source=google, utm_medium=cpc, utm_campaign=spring-sale — all captured, all submitted, all visible in your form entries.
Then the real submissions started arriving. And the UTM fields were blank. Not all of them — maybe 40%, maybe 70%, maybe every single one. No source, no medium, no campaign. Just empty columns where your attribution data should be.
You re-test. Still works. You ask a colleague to test. Works for them too. But real visitor submissions keep arriving with nothing. You start to wonder if UTM tracking on WordPress forms is fundamentally broken.
It isn’t broken — but the hidden field approach has architectural weaknesses that make it unreliable under real-world conditions. This guide explains exactly why, walks you through diagnosis, and shows you the server-side alternative that eliminates the problem.
Who this guide is for: WordPress site owners and marketers running paid ads (Google Ads, Meta Ads, Microsoft Ads) who use form plugins like Contact Form 7, WPForms, Gravity Forms, or similar — and who need reliable lead source attribution to measure ROI.
TL;DR — Why Hidden Fields Fail and What Actually Works
Hidden field UTM tracking breaks because of page caching serving stale HTML, JavaScript delay/defer settings preventing your script from running, cookie consent banners blocking storage, AJAX form timing issues, UTM parameters vanishing during navigation, iframe sandboxing, and click ID handling gaps. These aren’t edge cases — they affect the majority of WordPress sites running performance or privacy plugins. The fix is to move UTM capture server-side: a lightweight script sets a first-party cookie on page load, and PHP reads that cookie directly at form submission time — no hidden fields, no DOM population, no race conditions.
How Hidden Field UTM Tracking Is Supposed to Work
Before diagnosing what goes wrong, you need to understand the chain of events that must happen — in the right order, without interruption — for hidden field tracking to succeed.
A visitor clicks your ad and lands on your site with UTM parameters in the URL. A JavaScript file loads and executes on the page. That script reads the query string, extracts the UTM values, and stores them — typically in a browser cookie or localStorage. When the visitor navigates to a page with a form, the same script (or another one) reads the stored values and writes them into hidden <input> fields inside the form markup. The visitor fills out the visible fields and submits. The form plugin collects all field values — visible and hidden — and saves the entry.
Four things must be true simultaneously: JavaScript must execute, the browser must allow cookie or storage writes, the DOM must be ready and modifiable, and the form submission must include the populated hidden fields. Break any one link in that chain and you get blank values.
The reason your tests pass is that you’re testing under ideal conditions: a fresh browser session with no cached pages, cookies enabled, no consent banner blocking storage, and no JavaScript optimisation plugins deferring your tracking script. Real visitors rarely arrive under those conditions.
Learn more about how lead source tracking works under the hood.
The 7 Reasons Your Hidden Fields Are Coming Through Empty
Each of these causes is independently sufficient to wipe out your UTM data. Most WordPress sites have two or three of them active at the same time, which is why the failure rate can be so high.
1. Page Caching Is Serving a Stale Version of Your Form
This is the single most common cause of blank UTM hidden fields, and the most misunderstood. Page caching saves a static HTML snapshot of your page and serves it to every subsequent visitor. That snapshot includes the form — with empty hidden fields baked into the HTML.
Here is the sequence: Visitor A arrives, your JavaScript executes, and the hidden fields get populated for that specific page render. The caching plugin may or may not capture this state. Visitor B arrives and receives the cached HTML. If the cache was built before JavaScript ran (which is how server-side caching works), Visitor B gets empty hidden fields. The JavaScript then needs to execute on Visitor B’s browser to fill them — but that depends on all the other links in the chain holding up.
The problem spans multiple layers. WordPress caching plugins like WP Rocket, LiteSpeed Cache, WP Super Cache, and W3 Total Cache all generate full-page HTML caches. Managed hosting providers including WP Engine (Varnish), Kinsta (server-level cache), and Cloudways (Varnish + Redis) apply server-level caching that operates before WordPress even loads — you cannot fully control it from within WordPress. CDN layers like Cloudflare cache pages at the edge, meaning even if you purge your WordPress cache, Cloudflare may still serve the stale version from a data centre closest to your visitor.
The standard quick fix is to exclude your form pages from caching. Every caching plugin has a URL exclusion setting. But this fix introduces its own problems: those form pages no longer benefit from caching, which slows them down. You also need to remember to exclude every new page that contains a form — forever. Miss one and you are back to blank fields with no error to alert you.
On managed hosting, the exclusion is even less reliable. WP Engine’s Varnish cache respects some plugin-level cache-control headers but not all. Kinsta requires you to contact their support team to add server-level cache exclusions — you cannot configure this from within WordPress. Cloudflare needs a separate Page Rule or Cache Rule. You are maintaining exclusion lists in three different places for a single form page.
See how caching affects lead source tracking across different WordPress setups.
2. JavaScript Delay and Defer Block Your Tracking Script
Performance optimisation plugins have become aggressive about controlling when JavaScript executes — and your UTM tracking script is caught in the crossfire.
WP Rocket’s “Delay JavaScript Execution” feature is the most impactful. It does not simply defer or async-load your scripts. It prevents scripts from executing entirely until a user interaction occurs: a scroll, a click, a keypress, or a touch event. If a visitor lands on your page, reads the form, fills it out, and submits — all without scrolling — your tracking script never fires. The hidden fields remain empty because the JavaScript that populates them literally never ran.
LiteSpeed Cache has a similar “Load JS Deferred” option. Autoptimize can combine and defer all JavaScript files. Flying Scripts delays specific scripts based on URL patterns. NitroPack applies aggressive lazy-loading to scripts. Each of these can prevent your UTM capture code from executing before the form is submitted.
The quick fix is to add your tracking plugin’s JavaScript file to the exclusion list in your performance plugin. In WP Rocket, this is under Settings > File Optimization > Delay JavaScript Execution > Excluded JavaScript Files. In LiteSpeed Cache, it is under Page Optimization > JS Settings > JS Exclude. But this requires you to know the exact script handle or file path, and you need to verify the exclusion actually works — some plugins have bugs where excluded scripts still get delayed under certain conditions.
There is a deeper issue: every time your performance plugin updates, these exclusion settings can change behaviour. WP Rocket’s delay feature has had multiple changes across versions in how it handles excluded scripts. You are building your attribution tracking on top of a setting that another plugin’s update can silently break.
3. Cookie Consent Banners Silently Block UTM Storage
If your site serves visitors in the EU, UK, or any jurisdiction with cookie consent requirements, you almost certainly run a consent management platform (CMP). And that CMP is probably blocking your UTM tracking cookies before the visitor ever opts in.
Tools like Cookiebot, Complianz, CookieYes, and GDPR Cookie Consent work by intercepting JavaScript that sets cookies. They categorise each cookie by purpose: strictly necessary, functional, analytics, or marketing. UTM tracking cookies fall into the marketing or analytics category. Under GDPR, ePrivacy Directive, and similar regulations, these categories require explicit visitor consent before the cookie can be set.
When a visitor lands on your site and the consent banner appears, your tracking script may attempt to set a cookie with the UTM values. The CMP intercepts that attempt and blocks it — silently. No error in the browser console. No visual indication. The cookie simply does not get written. When the visitor later reaches your form, the script tries to read UTM values from a cookie that does not exist, and the hidden fields remain empty.
Some site owners try to reclassify UTM cookies as “functional” or “strictly necessary” to bypass consent requirements. This carries real compliance risk. A UTM tracking cookie’s purpose is marketing attribution — categorising it as functional is difficult to defend in a regulatory audit. First-party cookies with a clear functional purpose (like session management) have a stronger legal argument, but a cookie whose sole purpose is tracking which ad campaign a visitor clicked is marketing by any reasonable interpretation.
The consent rates vary by industry and geography, but studies from Cookiebot’s own data suggest that opt-in rates for marketing cookies typically range from 30% to 60% in European markets. That means 40% to 70% of your EU visitors will never have their UTM data stored, regardless of how well your hidden field implementation works.
4. Your Form Submits Before Hidden Fields Are Populated
This one trips people up because they conflate two different issues. AJAX form submission itself does not strip hidden field values. The problem is timing: if the JavaScript that populates your hidden fields has not yet executed when the visitor submits the form, those fields are submitted empty.
Most WordPress form plugins now submit via AJAX by default. Contact Form 7 uses AJAX submission by default. WPForms uses AJAX submissions by default. Gravity Forms 2.5 significantly updated its AJAX and rendering system. Ninja Forms is entirely JavaScript-rendered. Fluent Forms supports AJAX natively. The form submits without a full page reload, which is faster and better for user experience — but it means the submission happens within the current DOM state.
If your UTM population script is loaded with DOMContentLoaded but a performance plugin has delayed it (see reason #2), a fast-typing visitor can complete and submit the form before your script runs. The AJAX submission grabs the current field values — including the still-empty hidden fields — and sends them to the server.
There is a second timing problem. Some AJAX-based form plugins re-render part of the DOM after submission to show a success message. If your page has multiple forms or if the visitor refreshes and submits again, the re-rendered form markup may contain fresh, empty hidden fields that replace the populated ones.
Dig deeper into how AJAX form submissions affect ad tracking.
5. UTM Parameters Vanish When Visitors Navigate to the Form Page
UTM parameters exist in the URL. The moment a visitor navigates to a different page on your site, those parameters disappear from the address bar. If your form lives on a dedicated “Contact Us” page but the visitor landed on your homepage or a blog post, the form page URL has no UTM parameters to read.
This is exactly why the standard approach uses cookies: capture the UTM values on the landing page, store them in a cookie, and read from the cookie on the form page. But cookie storage depends on JavaScript executing successfully and the browser allowing the write — which loops back to reasons 1, 2, and 3.
There is another mechanism that strips UTM parameters even when the visitor does not navigate: 301 redirects. WordPress and many SEO plugins enforce trailing slashes or www/non-www consistency via 301 redirects. Some server configurations redirect HTTP to HTTPS. Each redirect can strip query parameters. A visitor clicking an ad that lands on https://example.com/contact?utm_source=google may be redirected to https://example.com/contact/ — note the trailing slash and the missing query string. Your JavaScript cannot read what is no longer there.
You can verify this by checking your server’s redirect behaviour. Open your browser’s developer tools, go to the Network tab, and navigate to your landing page URL with UTM parameters. If you see a 301 response before the 200 response, check whether the Location header in the 301 preserves the query string.
Learn why gclid and other click IDs vanish between page loads.
6. Embedded Forms and iFrames Cannot Read the Parent Page
If you embed a form using an iframe — whether from Typeform, HubSpot, Calendly, or any external form service — the embedded form cannot access the parent page’s URL or cookies. This is a browser security feature called the same-origin policy, and it is not a bug you can fix.
The iframe is sandboxed. It runs in its own browsing context with its own document, its own cookies, and its own URL (usually on the form provider’s domain). JavaScript inside the iframe cannot read window.parent.location or read document.cookie from the parent frame when the origins differ. Your UTM parameters and any cookies set on your domain are invisible to the embedded form.
The workaround is to pass UTM values as URL parameters in the iframe’s src attribute. For example: <iframe src="https://forms.typeform.com/to/abc123?utm_source=google&utm_medium=cpc">. This requires you to dynamically build the iframe URL with JavaScript on the parent page — which brings you back to the same JavaScript execution dependencies described above.
Read more about tracking limitations with embedded forms and iframes.
7. Click IDs Have Different Rules Than UTM Parameters
UTM parameters are standardised and human-readable. Click IDs are platform-specific, opaque, and have technical quirks that many tracking implementations do not account for.
| Click ID | Platform | Notes |
|---|---|---|
gclid | Google Ads | Can be up to 100 characters; required for offline conversion imports |
fbclid | Meta (Facebook/Instagram) | Appended automatically by Meta to all ad clicks |
wbraid | Google Ads (iOS web) | Privacy-compliant modelled click ID for iOS Safari users |
gbraid | Google Ads (iOS app) | Used for app conversion attribution on iOS |
msclkid | Microsoft Ads | Microsoft’s equivalent of gclid |
Many UTM tracking plugins and custom hidden field implementations only look for utm_source, utm_medium, utm_campaign, utm_term, and utm_content. They ignore click IDs entirely. If you are running Google Ads with auto-tagging enabled (which is the default), your ads may not even include UTM parameters — Google relies on gclid instead. Your hidden fields capture nothing because there is nothing in the UTM namespace to capture.
The gclid value itself can be very long. Some cookie implementations truncate values or impose size limits that clip the gclid, making it useless for conversion import. You need a tracking solution that explicitly captures click IDs alongside UTM parameters and stores them at their full length.
Understand wbraid, gbraid, and why Google now uses multiple click IDs.
How to Diagnose Which Problem You Have
Before you change anything, identify which of the seven causes is affecting your site. You may have more than one. Run through this diagnostic in order — each step builds on the previous one.
- Test with a clean browser session. Open an Incognito or Private window. Navigate to your landing page with UTM parameters appended (e.g.,
?utm_source=test&utm_medium=test&utm_campaign=test). Open DevTools (F12), go to the Application tab, and check Cookies for your domain. Verify the UTM values were stored. If they were not, your issue is cookie consent blocking (reason 3) or JavaScript delay (reason 2). - Inspect the hidden fields. Navigate to your form page. In DevTools, go to the Elements panel. Find your form and locate the hidden input fields. Check their
valueattributes. If the values are empty, your population script did not execute. If the values are present, the problem is later in the chain (form submission or storage). - Submit the form and check the entry. Fill in the form and submit it. Go to your form plugin’s entry list in wp-admin and check whether the UTM values were saved. If the hidden fields were populated (step 2) but the entry is blank, your form plugin may be stripping hidden fields on AJAX submission, or there is a field mapping issue.
- Repeat with caching disabled. Temporarily purge your page cache (WP Rocket > Clear Cache, or equivalent). Repeat steps 1-3. If UTM values now appear in form entries, page caching is your primary issue (reason 1). Re-enable caching and verify the problem returns.
- Check JavaScript delay settings. In WP Rocket, go to Settings > File Optimization > Delay JavaScript Execution. If enabled, check the excluded files list. In LiteSpeed Cache, go to Page Optimization > JS Settings. Look for defer or delay toggles. Temporarily disable JS delay, clear cache, and test again. If UTM fields now populate, JavaScript delay is the culprit (reason 2).
Document which step revealed the failure. If the problem appears at step 1 (no cookie), focus on consent and JavaScript execution. If it appears at step 2 (no hidden field values), focus on script timing. If it appears at step 3 (values present but entry blank), investigate your form plugin’s AJAX handling. If it only resolves at step 4, caching is the root cause.
If you have run through this checklist and the problem keeps recurring — or resolves temporarily but comes back after a plugin update or cache purge — the issue is not configuration. It is architectural. The hidden field approach has too many dependencies. Keep reading for the fix.
The Fundamental Problem With Hidden Fields
Hidden field UTM tracking is not failing because of a single misconfiguration. It is failing because the architecture has too many external dependencies, and each one operates independently of the others.
The chain looks like this: JavaScript must load. JavaScript must execute (not be delayed or deferred). The browser must permit cookie writes (consent). The cookie must persist across page navigations. JavaScript on the form page must execute. The DOM must be modifiable. The hidden fields must be populated before the form submits. The form submission must include hidden field values. The form plugin must store those values.
That is nine sequential dependencies. A failure at any point produces blank UTM fields with no error message. Every WordPress core update, every caching plugin update, every privacy regulation change, and every form plugin version bump introduces the possibility of breaking one of those links. You are not maintaining a tracking system — you are maintaining a fragile chain of assumptions about how nine different pieces of software will behave together.
This is not a configuration problem. It is a design problem. And the solution is to reduce the number of dependencies.
The Better Approach: Server-Side Cookie Reading
The hidden field approach fails because it depends on client-side JavaScript to both store and retrieve UTM data, then populate DOM elements, all before form submission. Server-side cookie reading removes most of those dependencies.
Here is how the architecture works. A small, lightweight JavaScript file runs on page load. Its only job is to read UTM parameters and click IDs from the current URL and write them into a first-party cookie on your domain. That is the extent of the client-side work — no DOM manipulation, no hidden field population, no timing coordination with form plugins.
When a visitor submits a form, the form plugin triggers a server-side hook (a PHP action or filter). At that point, PHP reads the UTM cookie directly from the $_COOKIE superglobal. The values are extracted, sanitised, and attached to the form entry — all on the server. No hidden fields exist in the HTML. No JavaScript needs to run on the form page. No DOM race condition is possible.
This approach collapses the nine-dependency chain down to three: JavaScript sets a cookie on the landing page, the cookie persists in the browser, and PHP reads it on submission. The three most fragile steps — DOM population, hidden field inclusion in submission, and JavaScript execution on the form page — are eliminated entirely.
Comparison: Hidden Fields vs Server-Side Cookie Reading
| Factor | Hidden Fields | Server-Side Cookies |
|---|---|---|
| How it works | JS reads URL, stores in cookie, populates hidden inputs in DOM, form submits with values | JS reads URL, sets first-party cookie; PHP reads cookie at form submission |
| Caching compatibility | Breaks when page cache serves stale HTML; requires per-page exclusions | Cache-immune — PHP reads cookie at submission time regardless of cached HTML |
| AJAX compatibility | Timing-dependent — JS must populate fields before AJAX submission fires | Fully compatible — server reads cookie independently of AJAX vs standard submit |
| Cookie consent impact | Blocked if consent not given (cookie write prevented) | Same consent requirement for initial cookie write; but no secondary JS dependency on form page |
| JS delay/defer impact | Broken if population script is delayed past form submission | Only landing page script affected; form page needs no JS execution |
| Setup complexity | Must add hidden fields to every form, configure JS population, exclude from caching/delay | Install plugin, activate — no per-form configuration needed |
| Form plugin support | Requires hidden field support per form plugin; varies by plugin | Hooks into form plugin submission actions server-side; works across all supported plugins |
Notice that cookie consent still applies to the initial cookie write. Server-side reading does not bypass GDPR. What it does eliminate is every failure point after the cookie is set. If the cookie exists, the data will be captured — no matter what caching, JavaScript optimisation, or AJAX configuration is active on the form page.
TrueConversion: Server-Side Tracking Built for WordPress Forms
TrueConversion implements exactly this architecture. It installs as a standard WordPress plugin and hooks directly into the submission actions of 10 form plugins: Contact Form 7, WPForms, Gravity Forms, Ninja Forms, Formidable Forms, Fluent Forms, Elementor Pro Forms, Forminator, Jetpack Forms, and a shortcode fallback for anything else.
You do not add hidden fields to your forms. You do not write JavaScript. You do not configure cache exclusions. The plugin’s lightweight frontend script captures UTMs and all major click IDs — gclid, fbclid, wbraid, gbraid, and msclkid — into a first-party cookie. On form submission, PHP reads the cookie and stores the full attribution data with the entry. The free version covers all tracking and all form integrations.
See the full technical breakdown of server-side tracking. Or explore how it works with your specific form plugin.
What This Means for Your Google Ads ROI
Blank UTM fields are not just a reporting annoyance. They directly damage your paid advertising performance.
When UTM data is missing from form submissions, you cannot attribute leads to specific campaigns, ad groups, or keywords. Your cost-per-lead calculations are based on incomplete data. You may be scaling campaigns that produce low-quality leads while cutting campaigns that actually drive revenue — because the attribution data to tell them apart does not exist.
If you use Google Ads’ Smart Bidding strategies (Target CPA, Target ROAS, Maximise Conversions), the algorithm optimises based on conversion data you feed back. Missing gclid values mean you cannot import offline conversions. Smart Bidding then optimises for the conversions it can see — which are disproportionately the ones from uncached pages, non-consented visitors, and other edge cases. The algorithm learns from a biased sample, and your bid strategy drifts from reality.
Fixing your attribution tracking is not an optimisation. It is a prerequisite for every other optimisation you want to make.
Learn how to set up Enhanced Conversions for Leads on WordPress. Or read about why Smart Bidding optimises for junk leads and how to fix it. For a complete pipeline, see offline conversion tracking without a CRM.
Frequently Asked Questions
Do I need to disable caching entirely to track UTMs?
No. Disabling caching entirely would be a severe performance penalty. With hidden field tracking, you only need to exclude pages containing forms from the cache — but this is fragile and requires ongoing maintenance. With server-side cookie reading, caching has no effect on UTM capture because PHP reads the cookie at submission time, regardless of whether the page was served from cache. You can keep your full caching setup intact.
Will this work with Contact Form 7 / WPForms / Gravity Forms?
Server-side tracking plugins like TrueConversion hook into each form plugin’s submission action. Contact Form 7 uses the wpcf7_before_send_mail hook, WPForms uses wpforms_process_complete, and Gravity Forms uses gform_after_submission. These are server-side PHP hooks that fire regardless of whether the form submitted via AJAX or standard POST. If you are using hidden fields, you need each form plugin to support hidden field types and map them correctly — which varies significantly between plugins and versions.
What about GDPR — are UTM cookies personal data?
UTM parameter values themselves (e.g., “google”, “cpc”, “spring-sale”) are not personal data. However, click IDs like gclid and fbclid can be linked back to individual ad interactions and are generally treated as personal data under GDPR. The cookie that stores these values requires consent in most EU implementations. Server-side reading does not bypass this consent requirement — but it does ensure that when consent is given and the cookie is set, the data is reliably captured without additional JavaScript dependencies on the form page.
Can I track gclid and fbclid alongside UTMs?
Yes, but your tracking implementation needs to explicitly look for them. Standard UTM tracking scripts only look for parameters prefixed with utm_. Click IDs use their own parameter names (gclid, fbclid, wbraid, gbraid, msclkid). TrueConversion captures all of these by default — both UTM parameters and platform click IDs — without any additional configuration.
Does this work with WPForms Lite (free)?
Yes. Server-side tracking hooks into WPForms’ core submission action, which exists in both the free Lite version and the paid Pro version. You do not need WPForms Pro or any WPForms addon to capture UTM data with server-side tracking. Hidden field approaches, by contrast, often require the paid version of form plugins to add custom hidden fields.
Why do hidden fields work in testing but fail on my live site?
Testing typically happens with a fresh browser (no cached pages), JavaScript execution enabled (no delay/defer), cookies allowed (no consent banner — you have already accepted it or are using an admin session), and a direct page load (no redirect stripping parameters). Live visitors encounter cached pages, delayed scripts, consent banners they have not interacted with, and multi-page sessions where UTM parameters vanish after the first navigation. The gap between your test environment and your visitors’ real experience is where the hidden field approach breaks down.
Stop Debugging Empty Hidden Fields
TrueConversion captures UTM parameters and ad platform click IDs server-side — no hidden fields, no cache exclusions, no JavaScript timing issues. It works with 10 WordPress form plugins out of the box. The free version includes all tracking features.
Leave a Reply