Opens in a new tab
Home/Blog/GA4 Measurement Protocol limits: the cheat sheet

GA4 Measurement Protocol limits: the cheat sheet

Data Accuracy & AttributionPhillip StemannOctober 19, 2026

Last updated: October 22, 2026

Title card reading GA4 Measurement Protocol limits: the cheat sheet, on a dark slate background with an orange glow
In this article
Key takeaways
  • A GA4 Measurement Protocol (MP) request is an HTTPS POST to /mp/collect with measurement_id and api_secret in the URL and a JSON body that must contain client_id and an events array.
  • The hard limits to remember: 25 events per request, 25 parameters per event, 40-character event and parameter names, 100-character parameter values (500 on GA4 360), 25 user properties (24-character names, 36-character values), a body under 130 kB, and backdating of at most 72 hours.
  • Always send session_id and engagement_time_msec. Google says events without them may not fully show in Realtime and do not count correctly towards engagement metrics.
  • The production endpoint answers 2xx even for bad payloads. Use the validation endpoint /debug/mp/collect to see errors, and remember it does not check your measurement_id or api_secret.
  • Google describes MP as a way to augment gtag.js or Tag Manager, not replace them. It works best for events your server knows about: paid orders, saved leads, refunds.

The Measurement Protocol is the part of GA4 where the documentation is accurate and the behaviour still surprises people. The request returns success, the event never shows up, and nothing tells you why. Most of the time the cause is one of about a dozen small rules scattered over four Google pages.

This is the single page I wanted when debugging it: endpoint, required fields, every limit, reserved names, how sessions and consent behave, how to validate, and a step-by-step debugging order. I fetched Google’s overview, reference, sending events, validating events and use cases pages in October 2026 and tested the endpoints with curl. Anything unconfirmed is flagged. This post covers web streams (measurement_id plus client_id). App streams use firebase_app_id and app_instance_id instead.

Cheat sheet of GA4 Measurement Protocol limits: 25 events per request, 25 parameters per event, 40 character names, 100 or 500 character values, 25 user properties, 130 kB body, 72 hour backdating
The limits on one page. Source: Google’s Measurement Protocol reference and sending-events pages.

Where MP fits: augmenting gtag, not replacing it

Google’s overview states the intent plainly: MP is meant to “augment automatic collection through gtag, Tag Manager, and Google Analytics for Firebase, not to replace it.” A typical use is a payment confirmed by a webhook, which the browser never sees.

That has practical consequences. MP does not run GA4’s automatic collection, so no page views, scrolls or enhanced measurement events appear unless you send them. And per the overview, rules for creating or renaming events in the GA4 interface are not triggered by events sent through MP, so your server has to send the final event name itself.

Diagram: the browser sends gtag.js events and creates client_id and session_id; the server reuses those IDs and sends Measurement Protocol events to the same GA4 property
Browser and server feed the same property. The shared client_id and session_id are what let GA4 treat them as one visitor.

If you are weighing browser against server in general, see client-side vs server-side tracking in WordPress. For the WordPress-specific GA4 setup, read GA4 server-side tracking for WordPress. If GA4’s limits are pushing you to look elsewhere, see the best GA4 alternatives for WordPress.

Endpoint and required parameters

MP accepts HTTPS POST only. Google’s reference lists two endpoints:

  • https://www.google-analytics.com/mp/collect
  • https://region1.google-analytics.com/mp/collect if you want data collected in the EU

The URL needs two query parameters, and the body needs two fields:

Where Field Required What it is
URL measurement_id Yes (web) The web data stream’s ID, starting with G-. Not the same as the Stream ID.
URL api_secret Yes A secret you create in the GA4 interface.
Body client_id Yes Identifies the browser instance. Should match the ID the GA tag generated on your site.
Body events Yes Array of event objects, up to 25.
Body user_id No Your own identifier for a signed-in user. UTF-8 characters only.

Where to find the api_secret

Per Google’s sending-events page: in Google Analytics click Admin, then under Data collection and modification choose Data streams, select your stream, click Measurement Protocol API secrets, then Create, give it a nickname and copy the value. The Measurement ID sits in the same stream’s details panel.

Treat the secret like a password. Google warns not to put it in client-side code, because anyone holding it can send arbitrary or spam data into your property. Send MP calls from a server, and use gtag.js or Tag Manager in the browser.

Getting the client_id

The reference recommends the format of two positive numbers joined by a period, or the full value of the client ID cookie. With gtag.js, Google’s tag reference documents a get command:

gtag('get', 'G-XXXXXXXXXX', 'client_id', (clientID) => {
  // pass clientID to your server, e.g. in a hidden field or a fetch call
});

Google’s reference says get also returns the session_id. I did not find a worked session_id example in the docs, so test that call on your own site before relying on it.

Payload structure

Here is a complete request. Replace the placeholders with your own values and send it from a server.

curl -X POST \
  "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "1234567890.1790000000",
    "user_id": "customer_77",
    "consent": {
      "ad_user_data": "GRANTED",
      "ad_personalization": "GRANTED"
    },
    "events": [{
      "name": "purchase",
      "params": {
        "session_id": "1790000100",
        "engagement_time_msec": 100,
        "transaction_id": "order_1042",
        "currency": "USD",
        "value": 59.00
      }
    }]
  }'
Annotated JSON payload for a GA4 purchase event with numbered notes on client_id, user_id, consent, events, session_id, engagement_time_msec and value
The same payload, line by line.

Other optional body fields in the reference include timestamp_micros (microseconds, not milliseconds), user_properties, user_data (user-provided data), user_location and ip_override (geographic data; user_location wins if both are present), device and user_agent (ignored if device is present), and validation_behavior.

The limits, with the details the cheat sheet cannot fit

Limit Value
Events per request 25
Parameters per event 25
User properties 25 (the reference says per request; the limitations list says per event; plan for 25 either way)
Event name 40 characters or fewer, alphanumeric and underscores only, must start with a letter
Parameter name (including item parameters) 40 characters or fewer, same character rules
Parameter value 100 characters standard; 500 on a GA4 360 property
User property name / value 24 / 36 characters
Custom parameters per item 10
POST body smaller than 130 kB
Backdating up to 72 hours
Non-conversion requests 100 million per hour per property (not applied to 360)

Three details trip people up:

  • Over-limit values are silently dropped by default. In the default RELAXED mode, MP “ignores parameters that exceed limits” instead of rejecting the request. A 140-character page_title will not cause an error; the parameter just vanishes. ENFORCE_RECOMMENDATIONS rejects them instead.
  • Old timestamps are moved, not refused. In relaxed mode an event older than 72 hours is accepted but its timestamp is overridden to 72 hours ago. In strict mode it is rejected.
  • The 100 million per hour limit is silent too. Past it, MP ignores non-conversion requests for the rest of the hour. You will not hit this on a typical WordPress site.

Timestamps follow a hierarchy: the event’s timestamp_micros, then the request’s, then the time MP received the request. Google also says events meant to be joined with gtag.js events should reach GA4 within 48 hours of the original client-side event, or they “may not be processed as expected, particularly for purposes like conversion attribution.”

Reserved names you cannot send

Validation returns NAME_RESERVED for these, per the reference:

  • Event names (31 on the web list, as I count it): ad_activeview, ad_click, ad_exposure, ad_query, ad_reward, adunit_exposure, app_clear_data, app_exception, app_install, app_remove, app_store_refund, app_update, app_upgrade, dynamic_link_app_open, dynamic_link_app_update, dynamic_link_first_open, error, firebase_campaign, firebase_in_app_message_action, firebase_in_app_message_dismiss, firebase_in_app_message_impression, first_open, first_visit, notification_dismiss, notification_foreground, notification_open, notification_receive, notification_send, os_update, session_start, user_engagement. Three more (ad_impression, in_app_purchase, screen_view) are restricted to app streams.
  • Parameter names: firebase_conversion, plus anything starting with _, firebase_, ga_, google_ or gtag.
  • User property names: first_open_time, first_visit_time, last_deep_link_referrer, user_id, first_open_after_install, plus anything starting with _, firebase_, ga_ or google_

Note session_start and first_visit are on the list. You cannot create a visit by sending them; more on that next.

Sessions, engagement time and the “(not set)” problem

MP has no memory of the browser. Whatever GA4 knows about a visit has to come from the IDs you send.

Google’s docs say that creating a new session_id creates a new session without sending session_start. So if you invent a session ID, you get a new session. If you send the real one from the visitor’s browser, the event joins the real session. Google’s use-case page spells out the condition for matching session attributes: include the session_id and send the request no later than 24 hours after the start of the online session.

engagement_time_msec matters for a different reason. Google’s tip: include both session_id and engagement_time_msec for accurate session and engagement metrics, “including Realtime”. Events sent without them may still appear in some reports, but they “won’t contribute correctly to metrics like Average engagement time or Engaged sessions,” and may not be fully reflected in Realtime user counts.

Put together, three server-side mistakes produce odd-looking data:

  1. A made-up client_id (for example a random UUID or an order ID). GA4 sees a brand-new user with no history. The event is real, but it is not connected to the ad click or the visit that caused it.
  2. A missing or invented session_id. A missing one means the event is not tied to any session; an invented one starts a session of its own.
  3. No engagement_time_msec. The event arrives but contributes zero engagement time.

Practitioners report such events landing under “(not set)” or “Unassigned”; I could not find Google documenting that. Simo Ahava’s experiment on MP session attribution shows events inheriting a session’s source and medium when client ID, session ID and timestamp line up, and cautions that isolated hits add noise. It is a proof of concept, not Google documentation.

How to attribute server events

The reliable method is the one Google documents: send the browser’s client_id and session_id with the server event. The capture step looks like this on a WordPress page:

  1. When the visitor converts (form submit, checkout), read client_id and session_id from the GA4 cookies or with gtag('get', ...).
  2. Store them with the order or lead, for example as order meta.
  3. When your server later confirms the conversion, send the MP event with those stored IDs.

I could not verify that MP accepts campaign parameters (source, medium, campaign) on server events and applies them to the session. The pages I fetched do not document it, so I am not recommending it. For Google Ads, Google’s use-case page says offline events exported to Ads need no session ID but must arrive within 63 days of the latest online event, with any timestamp override inside the last 72 hours. That is a separate path from session attribution; see Google Ads offline conversions for WordPress leads.

Key events and consent

Key events. Google’s help page says “Any event you collect can become a key event,” by marking it as one in GA4. Its pages do not single out MP events, so test it: send an MP event, then confirm it can be marked and appears in key event reporting. One MP-specific effect is documented: requests containing a key event that is a conversion in Google Ads are exempt from the 100 million requests per hour limit. Google also notes that attribution credit for key events can change for up to 12 days after recording, so early numbers move.

Consent. The body accepts a consent object with ad_user_data and ad_personalization, each GRANTED or DENIED. If you do not send it, Google says GA4 “uses the consent settings from corresponding online interactions for the client or app instance.” The older non_personalized_ads boolean is deprecated in favour of consent.ad_personalization.

Two limits of what this means. The object covers the two advertising signals, not the full consent state of your site. And sending an event server-side does not remove the need for a lawful basis to collect it; that question gets its own treatment in the consent posts of this series, starting with Consent Mode v2 on WordPress.

The validation server

The production endpoint tells you almost nothing. When I posted a payload with an invalid event name to /mp/collect, it returned HTTP 204 with an empty body. Google’s reference confirms it returns a 2xx code if the request was received, whatever the payload.

The validation endpoint is https://www.google-analytics.com/debug/mp/collect (EU: https://region1.google-analytics.com/debug/mp/collect). It takes the same URL parameters and body. Be careful with the path: Google’s validating-events page renders it in a way that can look like /_debug_/mp/collect, but the code samples use /debug/mp/collect, and a request to the underscore version returned 404 when I tested.

curl -X POST \
  "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "1234567890.1790000000",
    "validation_behavior": "ENFORCE_RECOMMENDATIONS",
    "events": [{ "name": "_bad", "params": {} }]
  }'

Response:

{
  "validationMessages": [ {
    "fieldPath": "events",
    "description": "Event at index: [0] has invalid name [_bad]. Names must start with an alphabetic character.",
    "validationCode": "NAME_INVALID"
  } ]
}

A clean payload returns { "validationMessages": [] }. The documented codes are VALUE_INVALID, VALUE_REQUIRED, NAME_INVALID, NAME_RESERVED, VALUE_OUT_OF_BOUNDS, EXCEEDED_MAX_ENTITIES and NAME_DUPLICATED.

Two warnings straight from Google. First: “The validation server does not validate the api_secret or measurement_id.” I sent obviously fake credentials and still got a normal validation answer, so a green result proves the payload is well-formed, nothing more. Second, events sent to the validation server do not appear in reports. Use ENFORCE_RECOMMENDATIONS while validating (it catches wrong types, oversized values and old timestamps), and leave validation_behavior out when you send live data so less gets rejected.

Why events do not show up: the usual suspects

  1. The payload is invalid, and production hides it. Reserved name, wrong type, over-limit value. Validate first.
  2. Wrong measurement_id or api_secret. Both endpoints accept them silently. Compare character by character with Admin > Data streams.
  3. Wrong client_id. Events land on a visitor who never visited.
  4. Missing session_id / engagement_time_msec. Events may be absent from Realtime or from engagement metrics.
  5. Timestamps too old. More than 72 hours back and the timestamp is rewritten (or the event rejected in strict mode). Joining with browser events after 48 hours risks attribution trouble.
  6. Looking in the wrong report. Google’s data freshness help says processing “can take 24-48 hours” and that Realtime covers fewer features than other reports. A server event may show in Realtime today and take a day to reach standard reports.
  7. Filters and typos. Internal-traffic filters, or a misspelt event name in your search.

Debugging step by step

Flowchart: validate with the debug endpoint, check measurement ID and API secret by hand, send for real, check Realtime, then wait for standard reports
Work top to bottom. Most problems are caught by step 1 or 2.

In short: validate with /debug/mp/collect and ENFORCE_RECOMMENDATIONS; check the credentials by hand; send to /mp/collect using a client_id and session_id copied from your own browser, with no timestamp_micros while testing; look in Realtime; then wait 24 to 48 hours for standard reports.

Google’s reference page also mentions the Event Builder in GA4 for composing and validating a request interactively.

A WordPress-flavoured example

A lead form on a WordPress site. The visitor submits it; your server saves the lead. A browser-only setup depends on the thank-you page loading and gtag firing. With MP, your server sends generate_lead itself, using the client_id and session_id captured in hidden form fields. Browser events keep flowing for the rest of the visit, and the server event covers the moment your server is sure of. The same event sent from both sides can double count, so keep one sender per event. GA4 deduplication specifics are outside what I verified here.

For shops, the same pattern applies to paid orders and refunds; see WooCommerce server-side tracking, and if GA4 still disagrees with your order count, why Meta, Google and GA4 never match your WooCommerce orders.

Frequently asked questions

Is the Measurement Protocol a replacement for gtag.js?

No. Google says it is meant to augment gtag, Tag Manager and Firebase collection. It does not collect page views or other automatic events, and event rules in the GA4 interface do not run on MP events.

How many events can I send in one request?

Up to 25, each with up to 25 parameters, in a body smaller than 130 kB. Names are limited to 40 characters and parameter values to 100 (500 on GA4 360).

Why does my request return 204 but nothing appears in GA4?

Because the production endpoint returns a success code for any request it receives. Validate with /debug/mp/collect, double check measurement_id and api_secret yourself, and include session_id and engagement_time_msec.

How far back can I send events?

Up to 72 hours. In the default relaxed mode older timestamps are moved to 72 hours ago; in strict mode they are rejected. For events that should join browser data, Google advises delivery within 48 hours.

Do I need a session_id?

For events that should belong to the visitor’s real session, yes, and Google says to send it within 24 hours of the session start. For offline events exported to Google Ads, Google says it is not required.

Related reading

If you would rather not maintain payloads and IDs yourself, Traculo’s WordPress plugin forwards events to GA4 through the Measurement Protocol, alongside other platforms, and keeps a delivery log with each integration’s response. Traculo sends the visitor’s client_id and the event’s original timestamp but no session_id, so its server events are not tied to a browser session. See server-side event tracking to check whether it fits your setup.

ShareLinkedInXEmail

About the author

Track what matters, without the guesswork

Traculo gives WordPress sites analytics, funnels and server-side event forwarding in one dashboard. Start on the free plan with 10,000 events a month.

Try Traculo free

Related articles

Title card reading GCLID, GBRAID and WBRAID: what they are and when each appears, on a dark slate background with an orange glow

GCLID, GBRAID and WBRAID: what they are and when each appears

What Google's click IDs are, when gclid, gbraid and wbraid appear, the upload rules, how WordPress loses them and how to capture them reliably.

Read more
Title card reading Google Ads offline conversions for WordPress leads on a dark slate background with an orange glow

Google Ads offline conversions for WordPress leads

Capture GCLIDs on WordPress, store them with each lead and upload qualified leads to Google Ads. Formats, limits, hashing and common errors explained.

Read more
Title card: One reference table, dedup rules and limits on every ad platform, on a dark slate background with an orange glow

Conversions API dedup rules and limits: Meta, TikTok, Snapchat, Pinterest, LinkedIn

One reference table of deduplication keys, windows, batch limits, event age, click IDs and hashing for Meta, TikTok, Snap, Pinterest, LinkedIn, Google.

Read more