Skip to content

Rate limits and missing logs

A burst of 50 logs per tab, one more every 60 seconds, and an error that fires 200 times arrives as 3 full copies and one summary line.

Several gates decide whether a log is written. All have defaults, and the defaults hold things back. Read this before you conclude something is broken.

Gate Default Where
Log limit bursts of up to 50 logs, then one log recharges every 60 s. The last 10 are for errors only client, per browser tab
Duplicates 3 full copies of the same error, then counted and sent as a summary client, per browser tab
Client severity floor WARNING in production, DEBUG in dev client, minSeverity
Server severity floor WARNING in production, DEBUG in the emulator function, minSeverity
Function concurrency maxInstances: 1 on createClientLogFunction function

A fixed budget can run out before the error that matters. Fifty logs per page load is spent by mid-morning in an app someone keeps open all day, so the crash at lunch is not sent. This budget refills, and it keeps a fifth of itself for errors.

src/main.ts
import { initLogger } from '@dasasian/firebase-structured-logger/client'
import { httpsCallable } from 'firebase/functions'
import { functions } from './config/firebase'
export const logger = initLogger({
appId: 'my-app',
releaseId: import.meta.env.VITE_RELEASE_ID ?? 'dev',
logFunction: httpsCallable(functions, 'logFrontendEvent'),
minSeverity: 'INFO',
rateLimitOptions: {
burstLimit: 50,
rechargeSecondsPerLog: 60,
reservedForErrors: 10,
duplicateLimit: 3,
summaryIntervalMinutes: 60,
summaryMaxAgeDays: 7,
maxPendingSummaries: 50,
},
})
Option Meaning
burstLimit how many logs can go at once
rechargeSecondsPerLog after a burst, seconds before one log comes back
reservedForErrors of the burst, how many only ERROR and above may use
duplicateLimit full copies of one error before counting starts
summaryIntervalMinutes how often a running repeat count is sent
summaryMaxAgeDays how long an unsent summary waits on the device
maxPendingSummaries most summaries kept at once. The oldest go first

Each tab can send a burst of up to burstLimit logs. After that, one log recharges every rechargeSecondsPerLog seconds, one at a time, so a full burst takes 50 minutes to come back. A reload does not reset it: the tab keeps its count in sessionStorage.

A burst at start-up is fine, and a user who works all afternoon is not held back for long. A bug that logs in a loop is held to about 60 entries an hour per user. By our estimate, 1,000 users stuck in such a loop for a working month stay around the 50 GiB of Cloud Logging that each project gets free.

The last reservedForErrors logs (10) are kept for errors. Warnings can use the burst down to 10, and only ERROR and above can use the rest. A value over half of burstLimit is capped at half, with one warning, so warnings always have room. A noisy warning cannot use up the room a crash needs.

Two errors are the same when the message and the screen both match, so the same error on two screens is kept apart. The first 3 are sent in full, with their stack and breadcrumbs. After that the browser only counts them and sends one summary:

WARNING Repeated 197 more times: cart sync failed
labels.repeatOf="<repeatKey of the first copy>" labels.repeatCount="197"
labels.firstSeen="…" labels.lastSeen="…"

The count and window are also in the entry’s body under context.repeat. Each full copy has its own labels.repeatKey, and the summary’s repeatOf points at it. To find a full copy and its summary together:

labels.repeatKey="<key>" OR labels.repeatOf="<key>"
  • A summary is sent once an hour, when the tab is hidden, and once at startup for anything a previous visit left queued.
  • It is keyed by the error, the releaseId and the userId, so two releases or two people on one computer are never counted together.
  • It is timestamped at lastSeen, not when it was sent.
  • Unsent summaries are kept in localStorage. The next visit sends them with labels.sentLate="true". A summary leaves the queue only once it has been sent, so a failed attempt is retried and never sent twice.
  • At most 50 wait, and any older than 7 days are deleted rather than sent. They are error messages and may hold personal data.
  • Summaries do not count against the burst.

A summary is a WARNING with no stack, so Error Reporting sees the 3 full copies and not the summary. For the true count, add up labels.repeatCount in Cloud Logging.

The two rate limits say so in the browser console:

[fsl] Duplicate counted for the next summary: TypeError: cannot read 'id' | checkout
[fsl] Log limit reached — recharging, next log in about a minute
[fsl] Log limit: only errors can use the reserved logs now

The severity floors are silent. Both the client’s minSeverity and the function’s minSeverity return with nothing written and nothing logged about it.

So if an entry never arrived and there is no [fsl] warning in the console, it was a floor, not a limit. In production both default to WARNING, which drops DEBUG, INFO and NOTICE on the way out of the browser and again on the way into Cloud Logging. So an INFO can be dropped in two places.

Set the server floor in the functions entry file:

functions/src/index.ts
import { initLogger } from '@dasasian/firebase-structured-logger/functions'
initLogger({ appId: 'my-app', minSeverity: 'INFO' })

maxInstances: 1 is a cost guard on what is usually the busiest function in the system. Raise it if client logs are being dropped under load, and watch your Cloud Logging bill when you do.

functions/src/index.ts
import { createClientLogFunction } from '@dasasian/firebase-structured-logger/functions'
export const logFrontendEvent = createClientLogFunction({
bucket: 'my-app.firebasestorage.app',
maxInstances: 5,
})

Raise it when the logs show dropped entries, not from reading the code alone. User feedback is exempt from every limit on this page.

Made by Dasasian