@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/?post_type=lh-portfolio-doc&#038;p=149602>
  a sioc:Post ;
  dc:title "Limits and filters" ;
  dcterms:identifier 149602 ;
  dc:modified "2026-10-11T16:19:08Z"^^xsd:dateTime ;
  dc:created "2026-10-10T03:32:42Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-beacon/limits-and-filters/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-beacon/">LH Beacon</a></li></ul>
<p class="wp-block-paragraph">The collect route is public in the REST sense (no capability check), but every request has to pass these checks, in this order, or it is refused with the status shown and nothing is dispatched:</p>



<ul class="wp-block-list">
<li><strong>Size</strong> (413): the body may be at most 8192 bytes.</li>



<li><strong>Origin</strong> (403): the <code>Origin</code> header, or failing that the <code>Referer</code>, must be this site&#8217;s home or site URL host.</li>



<li><strong>Rate</strong> (429): at most 300 requests per IP per minute. The limit is generous because a club&#8217;s wifi can put many devices behind one IP. It is counted in the persistent object cache when there is one, otherwise in transients.</li>



<li><strong>Shape</strong> (400): the body must be JSON of the form <code>{ "nonce": "…", "beacons": { key: payload, ... } }</code>. At most 20 keys are dispatched; keys that are not valid slugs are skipped.</li>



<li><strong>Nonce</strong> (403, <code>lh_beacon_invalid_nonce</code>): the nonce LH Beacon printed into the page must be valid for the visitor the login cookie identifies, or for user 0 when logged out. It travels in the body, not the <code>X-WP-Nonce</code> header, so a stale one is refused by LH Beacon and never trips the REST API&#8217;s own cookie check.</li>
</ul>



<p class="wp-block-paragraph">A request that passes returns 204. Taken together, a beacon has to come from a page this site served to that visitor within the nonce&#8217;s lifetime. The checks are layered for defence in depth, but they are still not authorisation: a logged-out visitor&#8217;s nonce is the same for everyone and visible in the page source. Listeners must validate every payload and never grant anything on the strength of a beacon.</p>



<h2 class="wp-block-heading">Logging refusals</h2>



<p class="wp-block-paragraph">Every refusal is written to debug.log as a Warning tagged <code>[lh_beacon]</code>, in the standard LH format that LH Debug Log Viewer reads. The entry has <code>event</code> <code>beacon_refused</code>, a <code>reason</code> (<code>too_large</code>, <code>origin</code>, <code>rate_limited</code>, <code>invalid_shape</code>, <code>nonce_missing</code> or <code>nonce_invalid</code>), the site, the Origin and Referer headers and the body size. Nonce refusals add the user ID from the login cookie and the beacon keys; rate-limit refusals add the IP address. Payloads and the nonce itself are never logged.</p>



<p class="wp-block-paragraph">This is needed because REST refusals are otherwise invisible. LH WP Die Logger reports nonce failures only when <code>wp_die()</code> runs, which a REST route never does, and WordPress does not announce a missing nonce at all. Logging only happens when <code>WP_DEBUG</code> is on.</p>



<h2 class="wp-block-heading">Page caching</h2>



<p class="wp-block-paragraph">A WordPress nonce is valid for 12 to 24 hours, and a cached page carries the nonce it was built with. Pages must therefore not be served from a full-page cache for longer than 12 hours, or beacons from them are refused. On LocalHero the LiteSpeed public cache TTL is 12 hours (43200 seconds) and the private (logged-in) cache 30 minutes. A beacon from a page left open past its nonce&#8217;s lifetime is refused, so a little long-tail data is lost by design. A run of <code>nonce_invalid</code> entries is the sign that a cache is serving pages older than that.</p>



<h2 class="wp-block-heading">Filters</h2>



<ul class="wp-block-list">
<li><code>lh_beacon_return_max_bytes</code>: largest body accepted (default 8192).</li>



<li><code>lh_beacon_return_max_keys</code>: most keys dispatched per request (default 20).</li>



<li><code>lh_beacon_return_rate_limit</code>: requests per IP per window (default 300; 0 disables the limit).</li>



<li><code>lh_beacon_return_rate_window</code>: window in seconds (default 60).</li>



<li><code>lh_beacon_return_allowed_hosts</code>: hosts beacons may come from (default this site&#8217;s home and site URL hosts).</li>



<li><code>lh_beacon_require_nonce</code>: return false to stop checking the nonce. For emergencies only, such as a cache serving pages older than the nonce lifetime.</li>



<li><code>lh_beacon_get_user_id</code>: the user ID passed to listeners, after the nonce has been checked against the real cookie user; return 0 to anonymise.</li>



<li><code>lh_beacon_return_plugin_debugging_status</code>: return false to stop logging refusals.</li>



<li><code>lh_beacon_get_client_ip</code>: the IP used for rate limiting (default <code>REMOTE_ADDR</code>); filter it when the site is behind a proxy.</li>



<li><code>lh_beacon_return_script_handle</code> and <code>lh_beacon_return_rest_namespace</code>: the script handle (<code>lh-beacon</code>) and REST namespace (<code>lh-beacon/v1</code>). Changing either breaks consumers that use the defaults.</li>



<li><code>lh_beacon_init_plugin</code>: return false to switch the plugin off.</li>
</ul>
"""^^rdf:XMLLiteral ;
  sioc:content """Part of: LH Beacon
The collect route is public in the REST sense (no capability check), but every request has to pass these checks, in this order, or it is refused with the status shown and nothing is dispatched:




Size (413): the body may be at most 8192 bytes.



Origin (403): the Origin header, or failing that the Referer, must be this site&#8217;s home or site URL host.



Rate (429): at most 300 requests per IP per minute. The limit is generous because a club&#8217;s wifi can put many devices behind one IP. It is counted in the persistent object cache when there is one, otherwise in transients.



Shape (400): the body must be JSON of the form { "nonce": "…", "beacons": { key: payload, ... } }. At most 20 keys are dispatched; keys that are not valid slugs are skipped.



Nonce (403, lh_beacon_invalid_nonce): the nonce LH Beacon printed into the page must be valid for the visitor the login cookie identifies, or for user 0 when logged out. It travels in the body, not the X-WP-Nonce header, so a stale one is refused by LH Beacon and never trips the REST API&#8217;s own cookie check.




A request that passes returns 204. Taken together, a beacon has to come from a page this site served to that visitor within the nonce&#8217;s lifetime. The checks are layered for defence in depth, but they are still not authorisation: a logged-out visitor&#8217;s nonce is the same for everyone and visible in the page source. Listeners must validate every payload and never grant anything on the strength of a beacon.



Logging refusals



Every refusal is written to debug.log as a Warning tagged [lh_beacon], in the standard LH format that LH Debug Log Viewer reads. The entry has event beacon_refused, a reason (too_large, origin, rate_limited, invalid_shape, nonce_missing or nonce_invalid), the site, the Origin and Referer headers and the body size. Nonce refusals add the user ID from the login cookie and the beacon keys; rate-limit refusals add the IP address. Payloads and the nonce itself are never logged.



This is needed because REST refusals are otherwise invisible. LH WP Die Logger reports nonce failures only when wp_die() runs, which a REST route never does, and WordPress does not announce a missing nonce at all. Logging only happens when WP_DEBUG is on.



Page caching



A WordPress nonce is valid for 12 to 24 hours, and a cached page carries the nonce it was built with. Pages must therefore not be served from a full-page cache for longer than 12 hours, or beacons from them are refused. On LocalHero the LiteSpeed public cache TTL is 12 hours (43200 seconds) and the private (logged-in) cache 30 minutes. A beacon from a page left open past its nonce&#8217;s lifetime is refused, so a little long-tail data is lost by design. A run of nonce_invalid entries is the sign that a cache is serving pages older than that.



Filters




lh_beacon_return_max_bytes: largest body accepted (default 8192).



lh_beacon_return_max_keys: most keys dispatched per request (default 20).



lh_beacon_return_rate_limit: requests per IP per window (default 300; 0 disables the limit).



lh_beacon_return_rate_window: window in seconds (default 60).



lh_beacon_return_allowed_hosts: hosts beacons may come from (default this site&#8217;s home and site URL hosts).



lh_beacon_require_nonce: return false to stop checking the nonce. For emergencies only, such as a cache serving pages older than the nonce lifetime.



lh_beacon_get_user_id: the user ID passed to listeners, after the nonce has been checked against the real cookie user; return 0 to anonymise.



lh_beacon_return_plugin_debugging_status: return false to stop logging refusals.



lh_beacon_get_client_ip: the IP used for rate limiting (default REMOTE_ADDR); filter it when the site is behind a proxy.



lh_beacon_return_script_handle and lh_beacon_return_rest_namespace: the script handle (lh-beacon) and REST namespace (lh-beacon/v1). Changing either breaks consumers that use the defaults.



lh_beacon_init_plugin: return false to switch the plugin off.

""" ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
