{"id":17204,"date":"2026-10-10T07:20:00","date_gmt":"2026-10-10T05:20:00","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?p=17204"},"modified":"2026-10-09T22:41:29","modified_gmt":"2026-10-09T20:41:29","slug":"ga4-measurement-protocol-payloads-every-field-every-limit","status":"publish","type":"post","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/","title":{"rendered":"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON"},"content":{"rendered":"<p>A Measurement Protocol request is one HTTPS POST: a query string carrying two credentials, and a JSON body carrying the events. It is the way data reaches a GA4 property from a server rather than from a browser \u2014 offline conversions from a CRM, a payment confirmed by a webhook, an app backend, a bot-free replay of hits that an ad blocker swallowed.<\/p>\n<p>It is also the only ingestion path in the whole stack that never says whether it worked.<\/p>\n<figure class=\"lw-diagram\">\n<img decoding=\"async\" src=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/diagrams\/mp-nutzlast-en.png\" width=\"1120\" height=\"580\" loading=\"lazy\" alt=\"The anatomy of a Measurement Protocol payload with its documented limits on the left, and on the right a table of nine deliberately broken requests with the status code the production endpoint returned for each\"><figcaption>What a payload is made of, and what the production endpoint answers when parts of it are wrong.<\/figcaption><\/figure>\n<h2>What the endpoint answers<\/h2>\n<p>Nine requests went to <code>https:\/\/www.google-analytics.com\/mp\/collect<\/code> on 9 September 2026, all with a made-up measurement ID so that nothing could be recorded anywhere. Seven of them were deliberately broken.<\/p>\n<p>Eight of the nine came back <strong>204 No Content<\/strong> in about 100 milliseconds with an empty body. Not only the valid one: also the event whose name started with an underscore, the empty <code>events<\/code> array, the payload with an invented top-level field, the request with no <code>client_id<\/code>, and a body that was not JSON at all. A request sent with <strong>no query string whatsoever<\/strong> \u2014 no measurement ID, no API secret \u2014 also returned 204.<\/p>\n<p>Exactly two things produced an HTTP error, and neither is a verdict on the payload. A body of 140 kB was refused with <strong>413<\/strong> and an HTML error page from the front end, because it exceeded the documented 130 kB cap before any parsing happened. A <code>GET<\/code> was refused with <strong>405<\/strong>.<\/p>\n<p>The European endpoint <code>region1.google-analytics.com<\/code> behaves identically. Over five runs each, the median round trip from the same server was 90 milliseconds to both hosts.<\/p>\n<p>The response code therefore tells you only that the request arrived. It does not tell you whether the event was understood or stored, or whether a parameter was dropped. Those problems remain invisible when sending; they appear only as missing data in a report hours later.<\/p>\n<h2>Where the payload actually goes<\/h2>\n<p>Two parts, and they are easy to confuse.<\/p>\n<p>The <strong>query string<\/strong> carries the credentials: <code>measurement_id<\/code> for a web stream (or <code>firebase_app_id<\/code> for an app stream) and <code>api_secret<\/code>, which is created per stream in the GA4 admin. Neither belongs in the JSON. Placed there they become unknown fields, and an unknown field is not ignored \u2014 it makes the entire payload unreadable.<\/p>\n<p>The <strong>body<\/strong> is a JSON object with a fixed set of permitted keys:<\/p>\n<pre><code>{\n  \"client_id\": \"1234567890.1700000000\",\n  \"user_id\": \"crm-88213\",\n  \"timestamp_micros\": 1788000000000000,\n  \"consent\": { \"ad_user_data\": \"GRANTED\", \"ad_personalization\": \"DENIED\" },\n  \"user_properties\": { \"customer_tier\": { \"value\": \"gold\" } },\n  \"events\": [\n    {\n      \"name\": \"purchase\",\n      \"params\": {\n        \"transaction_id\": \"T-8891\",\n        \"currency\": \"EUR\",\n        \"value\": 20,\n        \"session_id\": \"1700000000\",\n        \"engagement_time_msec\": 100,\n        \"items\": [\n          { \"item_id\": \"A-1\", \"item_name\": \"Shirt\", \"price\": 10, \"quantity\": 2 }\n        ]\n      }\n    }\n  ]\n}<\/code><\/pre>\n<p>Besides these, the reference permits <code>user_data<\/code>, <code>user_location<\/code>, <code>ip_override<\/code>, <code>device<\/code>, <code>user_agent<\/code>, <code>non_personalized_ads<\/code> and <code>validation_behavior<\/code>. That is the complete list. Anything else \u2014 a stray <code>event<\/code> key copied from a dataLayer object, an <code>ecommerce<\/code> wrapper, a note to a colleague \u2014 costs the whole request.<\/p>\n<h2>The identity fields<\/h2>\n<p><code>client_id<\/code> is required on a web stream, and it is the single field that decides whether an event joins an existing user or invents a new one. The recommended form is two positive numbers joined by a period, which is what <code>gtag('get')<\/code> hands out and what the built-in Analytics Client ID variable returns in Google Tag Manager. The full <code>_ga<\/code> cookie value, <code>GA1.1.1234567890.1700000000<\/code>, is accepted as well; the two numbers are the part inside it.<\/p>\n<p>A value invented on the server is accepted too, and that is the expensive mistake. A fresh identifier per request produces a fresh user and a fresh session every time \u2014 a property that looks busy and consists entirely of one-event sessions with a 100 % engagement-free rate.<\/p>\n<p>App streams use <code>app_instance_id<\/code> instead: 32 hexadecimal characters from the Firebase SDK, not a client ID under another name. Sending both does no harm and no good; the field belonging to the other stream type is ignored.<\/p>\n<p><code>user_id<\/code> is optional, up to 256 characters, and must be a string. It is a join key, not a person: an email address or a phone number in that field is personal data, which the Google Analytics terms forbid and which is grounds for deleting a property. The same applies to every event parameter and every user property.<\/p>\n<h2>The two parameters that decide whether the reports add up<\/h2>\n<p>An event without <code>session_id<\/code> belongs to no session. It is still stored and still countable, but sessions, engaged sessions and the session-scoped dimensions do not add up, and the event may be missing from Realtime entirely.<\/p>\n<p>An event without <code>engagement_time_msec<\/code> contributes nothing to average engagement time, which then reads as zero for the traffic that arrived this way.<\/p>\n<p>Neither is required, neither produces a warning anywhere, and both are the usual answer to &#8220;the events arrive but the report is wrong&#8221;. <code>session_id<\/code> must be a positive integer \u2014 a timestamp in the ordinary case \u2014 and may be sent as a number or as a string of digits. Letters in it are refused.<\/p>\n<h2>The event and its name<\/h2>\n<p>An event is an object with exactly two permitted keys, <code>name<\/code> and <code>params<\/code>. A timestamp put beside them as a third key breaks the payload; the per-event timestamp goes inside <code>params<\/code> as <code>timestamp_micros<\/code>.<\/p>\n<p>Names must start with a letter and may contain only letters, digits and underscores. Hyphens, dots and spaces are refused outright. Names are <strong>case sensitive<\/strong>, which is the quiet one: <code>Purchase<\/code> is accepted, is stored, and is a different event from <code>purchase<\/code> \u2014 it will never reach the e-commerce reports, because those are wired to the lowercase name.<\/p>\n<p>Thirty-one names are reserved for Google&#8217;s own use, among them <code>session_start<\/code>, <code>first_visit<\/code>, <code>user_engagement<\/code>, <code>app_remove<\/code> and <code>error<\/code>. Three further names \u2014 <code>screen_view<\/code>, <code>ad_impression<\/code> and <code>in_app_purchase<\/code> \u2014 exist but are permitted on app streams only. On a web stream they are simply not collected, and nothing anywhere says so.<\/p>\n<h2>Parameters, and the limits that apply to them<\/h2>\n<p>Every limit in the protocol, in one place:<\/p>\n<table>\n<tr>\n<th>What<\/th>\n<th>Limit<\/th>\n<\/tr>\n<tr>\n<td>JSON body<\/td>\n<td>under 130 kB<\/td>\n<\/tr>\n<tr>\n<td>Events per request<\/td>\n<td>25<\/td>\n<\/tr>\n<tr>\n<td>Event name<\/td>\n<td>40 characters<\/td>\n<\/tr>\n<tr>\n<td>Parameters per event<\/td>\n<td>25<\/td>\n<\/tr>\n<tr>\n<td>Parameter name<\/td>\n<td>40 characters<\/td>\n<\/tr>\n<tr>\n<td>Parameter value<\/td>\n<td>100 characters, 500 on an Analytics 360 property<\/td>\n<\/tr>\n<tr>\n<td>User properties per request<\/td>\n<td>25<\/td>\n<\/tr>\n<tr>\n<td>User property name<\/td>\n<td>24 characters<\/td>\n<\/tr>\n<tr>\n<td>User property value<\/td>\n<td>36 characters<\/td>\n<\/tr>\n<tr>\n<td><code>user_id<\/code><\/td>\n<td>256 characters<\/td>\n<\/tr>\n<tr>\n<td>Items per event<\/td>\n<td>200<\/td>\n<\/tr>\n<tr>\n<td>Custom parameters per item<\/td>\n<td>10<\/td>\n<\/tr>\n<tr>\n<td>Backdating<\/td>\n<td>72 hours<\/td>\n<\/tr>\n<\/table>\n<p>Parameter names may not begin with <code>_<\/code>, <code>firebase_<\/code>, <code>ga_<\/code>, <code>google_<\/code> or <code>gtag.<\/code>, and <code>firebase_conversion<\/code> is reserved outright. User property names carry the same prefix rules plus five reserved names, of which <code>user_id<\/code> is the one people reach for by accident \u2014 the user ID belongs at the top level, not in <code>user_properties<\/code>.<\/p>\n<p>Only <code>items<\/code> may hold an array. Every other parameter takes a single value; a nested object handed to one is dropped, and the report shows the parameter as absent rather than as wrong.<\/p>\n<h2>Where the two maxima collide<\/h2>\n<p>Twenty-five events per request and two hundred items per event are both documented, and they cannot both be used. Measured on generated payloads with realistic item fields \u2014 ID, name, brand, category, variant, price, quantity:<\/p>\n<ul>\n<li>A <code>purchase<\/code> with one item, including consent and user properties, is <strong>558 bytes<\/strong>.<\/li>\n<li>Twenty-five such events come to roughly <strong>14 kB<\/strong>, which is 10.7 % of the cap.<\/li>\n<li>Twenty-five events with <strong>29 items each<\/strong> cross 130 kB. At 28 items the payload is still 126 kB.<\/li>\n<li>Twenty-five events with the permitted 200 items each would be 879 kB \u2014 <strong>6.8 times<\/strong> the cap.<\/li>\n<\/ul>\n<p>A single event with 200 items is 35 kB and fits comfortably. So the event count is the binding limit for ordinary traffic, and the size cap takes over as soon as baskets get long. The crossover sits at roughly 29 items per event, and it is the one limit that answers with an HTTP error instead of silence.<\/p>\n<h2>Time, and the three ways to get it wrong<\/h2>\n<p><code>timestamp_micros<\/code> is a Unix timestamp in <strong>microseconds<\/strong>. Omitted, the event is stamped on arrival, which is right for anything happening now and wrong for anything replayed.<\/p>\n<p>The unit is the trap. <code>time()<\/code> in PHP and <code>time.time()<\/code> in Python return seconds; <code>Date.now()<\/code> in JavaScript returns milliseconds. Both are accepted as numbers, and both put the event somewhere in 1970. Google&#8217;s validation server does report this \u2014 as &#8220;timestamp too far in the past&#8221;, a message that sends people looking for a backdating problem when the actual fault is a factor of a thousand or a million.<\/p>\n<p>The window is 72 hours. Anything older is discarded, and events sent with <code>validation_behavior<\/code> set to <code>ENFORCE_RECOMMENDATIONS<\/code> are rejected outright rather than dropped. A timestamp in the future usually means a clock out of step or a time zone added twice.<\/p>\n<h2>Consent, and the keys that do not belong here<\/h2>\n<p>The <code>consent<\/code> object takes exactly two keys, <code>ad_user_data<\/code> and <code>ad_personalization<\/code>, each of them <code>GRANTED<\/code> or <code>DENIED<\/code>. Not <code>analytics_storage<\/code>, not <code>ad_storage<\/code> \u2014 those are Consent Mode in the browser, a different mechanism with a different vocabulary.<\/p>\n<p>Putting one of them here is not a partial failure. An unknown key inside <code>consent<\/code>, or a value other than the two permitted ones, makes the payload unparseable and the whole request pointless. Left out entirely, GA4 falls back to the consent state from the browser interactions of the same client, which is usually what a server-side integration wants.<\/p>\n<p><code>non_personalized_ads<\/code> still works and is deprecated; <code>ad_personalization<\/code> inside <code>consent<\/code> replaces it.<\/p>\n<h2>Validation behaviour, and why production should not set it<\/h2>\n<p><code>validation_behavior<\/code> has two values. <code>RELAXED<\/code> is the default: malformed requests are rejected, but parameters that exceed the limits are ignored rather than reported, and data that is merely of the wrong type may still be accepted. <code>ENFORCE_RECOMMENDATIONS<\/code> rejects those instead.<\/p>\n<p>Use strict validation in testing, where a rejection helps identify a problem. In production, leave the field out: a rejection there means lost data. With relaxed validation, an event with one over-long parameter arrives without that parameter; with strict validation, the entire event is rejected.<\/p>\n<h2>Finding a fault at all<\/h2>\n<p>Since the production endpoint says nothing, faults have to be found before sending. Google&#8217;s validation server takes the same request at <code>\/debug\/mp\/collect<\/code> and answers with a <code>validationMessages<\/code> array; events sent there are never recorded.<\/p>\n<p>Before relying on the validation server, keep two limitations in mind. It reports <strong>only the first fault it finds<\/strong>: if an event has three problems, it returns one message, and the next appears only after the first is fixed. It also checks neither <code>measurement_id<\/code> nor <code>api_secret<\/code>. Invented credentials therefore produce exactly the same verdict as real ones. This is documented and lets you check a payload without sending a real secret outside the organisation.<\/p>\n<p>It is also worth knowing what it does not check at all. Measured against it with <code>ENFORCE_RECOMMENDATIONS<\/code> set, all of the following passed without a word: thirty events in one request, an empty <code>events<\/code> array, a payload with no <code>events<\/code> key, two events sharing a name, <code>screen_view<\/code> on a web stream, and a <code>purchase<\/code> with neither <code>value<\/code> nor <code>currency<\/code>. Without <code>validation_behavior<\/code> \u2014 that is, the way production treats the same text \u2014 a parameter value of 120 characters came back clean as well.<\/p>\n<h2>What the protocol is now<\/h2>\n<p>Google put the Measurement Protocol into a finalized state in June 2026: no deprecation, no new features. The documentation now recommends the Data Manager API for new server-to-server integrations, with OAuth instead of an API secret, several destinations per request and encrypted identifiers.<\/p>\n<p>For an existing integration that is not an emergency. The Measurement Protocol keeps working, its limits keep applying, and its endpoint keeps answering 204 to everything \u2014 which makes the checking that has to happen before sending the same job it always was.<\/p>\n<div class=\"lw-faq\">\n<h2>Questions and answers<\/h2>\n<h3>How can it be established in production whether Measurement Protocol events actually arrive?<\/h3>\n<p>Not from the response code, which is almost always 204. What works is a combination of checking before sending and reconciling afterwards:<\/p>\n<ol>\n<li>Every new or changed payload goes to <code>\/debug\/mp\/collect<\/code> first. Because the validation server reports only the first fault, the run has to be repeated until no message comes back.<\/li>\n<li>What the validation server does not check, the sending code checks itself: no more than 25 events per request, a non-empty events array, recommended event names such as purchase in lowercase, no app-only events on a web stream, value and currency on a purchase.<\/li>\n<li>The sender logs what it sent, for example the number of purchase events per day and their transaction_id values. Once the report is available a few hours later, the comparison shows whether events are missing.<\/li>\n<\/ol>\n<p>Only the third step also catches losses in production, for example after an API secret has been replaced or when a timestamp is in the wrong unit.<\/p>\n<h3>What happens when the API secret is deleted or replaced in the GA4 admin?<\/h3>\n<p>Nothing visible at the point of sending: in the measurement, the endpoint answered 204 even to a request with no credentials at all, and the validation server does not check measurement_id or api_secret. A sender still using an outdated secret therefore loses its events unnoticed. When a secret is replaced, all senders have to be switched over before the old one is deleted.<\/p>\n<h3>How do server-side events fit with the consent recorded in the browser through Consent Mode?<\/h3>\n<p>Only through the sending code. The Measurement Protocol consent object supports only ad_user_data and ad_personalization; there is no key for analytics_storage, and sending one anyway makes the payload unreadable. A refusal of analytics therefore cannot be passed to the endpoint through the consent object. The decision whether an event is sent for this client at all has to be made beforehand, in the sender&#8217;s own system.<\/p>\n<p>The identifier adds to this. With analytics_storage denied, the Google tag in the browser sets no _ga cookie, so a server reading the client_id from it finds none. A replacement identifier invented on the server would be exactly the expensive mistake: a new user with a session made of a single event.<\/p>\n<h3>Does a timestamp in milliseconds land in 1970 or not reach the reports at all?<\/h3>\n<p>It does not reach them, because it misses the 72-hour window. For the moment in the example payload, <code>Date.now()<\/code> returns 1,788,000,000,000. Read as microseconds, that is 1,788,000 seconds, about 20.7 days after the start of Unix time, which is 21 January 1970. A value in seconds lands just under half an hour after midnight on 1 January 1970. Both are older than 72 hours and are discarded, while the production endpoint keeps answering 204.<\/p>\n<\/div>\n<div class=\"lw-quellen\">\n<h2>Sources<\/h2>\n<ul>\n<li><a href=\"https:\/\/developers.google.com\/analytics\/devguides\/collection\/protocol\/ga4\" target=\"_blank\" rel=\"noopener noreferrer\">GA4 Measurement Protocol<\/a><\/li>\n<li><a href=\"https:\/\/support.google.com\/analytics\/answer\/9267744\" target=\"_blank\" rel=\"noopener noreferrer\">GA4 collection and configuration limits<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/analytics\/devguides\/collection\/ga4\" target=\"_blank\" rel=\"noopener noreferrer\">Google Analytics 4 developer guide<\/a><\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Nine requests, seven of them deliberately broken, went to the production Measurement Protocol endpoint. Eight came back 204 No Content in about 100 milliseconds &#8211; including the one that was not JSON at all and the one sent without any credentials. This is the complete anatomy of a payload: every permitted field, every documented limit, and the point where two of those limits cannot both be used.<\/p>\n","protected":false},"author":1,"featured_media":19744,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[91297,91258,91061],"class_list":["post-17204","post","type-post","status-publish","format-standard","hentry","category-digital-analytics","tag-data-quality","tag-google-analytics-4","tag-server-side-gtm"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"Every field and limit of a GA4 Measurement Protocol payload, measured: what the endpoint accepts silently, where 25 events and 200 items collide, and what the debug server does not check.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"Every field and limit of a GA4 Measurement Protocol payload, measured: what the endpoint accepts silently, where 25 events and 200 items collide, and what the debug server does not check.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-10T05:20:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Lukas Wojcik\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Lukas Wojcik\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"12 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/\"},\"author\":{\"name\":\"Lukas Wojcik\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"headline\":\"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON\",\"datePublished\":\"2026-10-10T05:20:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/\"},\"wordCount\":2249,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png\",\"keywords\":[\"Data Quality\",\"Google Analytics 4\",\"Server-Side GTM\"],\"articleSection\":[\"Digital Analytics\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/\",\"name\":\"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png\",\"datePublished\":\"2026-10-10T05:20:00+00:00\",\"description\":\"Every field and limit of a GA4 Measurement Protocol payload, measured: what the endpoint accepts silently, where 25 events and 200 items collide, and what the debug server does not check.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png\",\"width\":1200,\"height\":630,\"caption\":\"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/digital-analytics\\\/ga4-measurement-protocol-payloads-every-field-every-limit\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON | Lukas Wojcik","description":"Every field and limit of a GA4 Measurement Protocol payload, measured: what the endpoint accepts silently, where 25 events and 200 items collide, and what the debug server does not check.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/","og_locale":"en_US","og_type":"article","og_title":"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON | Lukas Wojcik","og_description":"Every field and limit of a GA4 Measurement Protocol payload, measured: what the endpoint accepts silently, where 25 events and 200 items collide, and what the debug server does not check.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/","og_site_name":"Lukas Wojcik - Blog","article_published_time":"2026-10-10T05:20:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png","type":"image\/png"}],"author":"Lukas Wojcik","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Lukas Wojcik","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#article","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/"},"author":{"name":"Lukas Wojcik","@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"headline":"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON","datePublished":"2026-10-10T05:20:00+00:00","mainEntityOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/"},"wordCount":2249,"commentCount":0,"publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png","keywords":["Data Quality","Google Analytics 4","Server-Side GTM"],"articleSection":["Digital Analytics"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/","name":"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#primaryimage"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png","datePublished":"2026-10-10T05:20:00+00:00","description":"Every field and limit of a GA4 Measurement Protocol payload, measured: what the endpoint accepts silently, where 25 events and 200 items collide, and what the debug server does not check.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#primaryimage","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/hero-17204-ga4-measurement-protocol-payloads-ev-dn.png","width":1200,"height":630,"caption":"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON"},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/digital-analytics\/ga4-measurement-protocol-payloads-every-field-every-limit\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"GA4 Measurement Protocol Payloads: Every Field, Every Limit, and Why the Endpoint Answers 204 to Broken JSON"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/17204","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=17204"}],"version-history":[{"count":3,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/17204\/revisions"}],"predecessor-version":[{"id":21799,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/17204\/revisions\/21799"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media\/19744"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=17204"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/categories?post=17204"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=17204"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}