AccoilAccoil Developer Docs
Best Practices

Cost Optimization

Strategies to reduce analytics costs without losing insight.

Most analytics platforms bill based on event volume, monthly tracked users (MTUs), or both. Every poorly designed event is a recurring charge for data nobody uses. This page covers concrete strategies to reduce costs without losing the insights that matter.

Strategy 1: Traits over event proliferation

When you need to distinguish variants of the same action, use traits (via identify or group) rather than creating separate events.

The problem

// Three events where one would suffice
track("Standard_Report_Created");
track("Custom_Report_Created");
track("Template_Report_Created");

Three events for the same action means three times the volume and three times the cost. Aggregate queries ("how many reports were created?") require combining all three. Add a fourth report type and you need a new event, new dashboard panels, and updated queries.

The fix

// One event, with context on the account
track("Report_Created");

// Report type context lives in group traits
group("acc_456", {
  primary_report_type: "standard",
  total_reports: 42,
});

Accoil specifics

Accoil's track call accepts only the event name -- no properties. This naturally enforces the "one event, use traits for context" pattern. Contextual metadata belongs on user traits (via identify) or account traits (via group), not on the event itself.

The impact

During tracking plan design, actively look for event families that can be consolidated. This is one of the highest-impact cost optimizations in event design. A tracking plan with 30 well-designed events outperforms one with 100 redundant variants.

Strategy 2: Instance-level tracking

In many B2B products, individual user identity is not needed for product analytics. The questions that matter are account-level: "Is this account adopting the feature?" not "Is this specific user adopting the feature?"

Instance-level tracking assigns a single shared user ID to all users within an account. Instead of tracking each user separately, all activity is attributed to one representative identity per account.

The cost impact

Most analytics platforms bill by MTU. Instance-level tracking reduces MTU counts dramatically:

ScenarioPer-user MTUsInstance-level MTUsReduction
100 accounts x 50 users5,00010098%
500 accounts x 200 users100,00050099.5%

Implementation

Use the account identifier as the user ID for all analytics calls:

// src/analytics/utils.js

export const getUserId = (context) => {
  // Instance-level: all users in an account share the account ID
  if (process.env.ANALYTICS_INSTANCE_LEVEL === "true") {
    return context.accountId;
  }
  // Per-user: each user has their own ID
  return context.userId;
};

This can be toggled via environment variable without code changes.

What you keep

  • Feature usage at the account level
  • Workflow and adoption metrics
  • Subscription and licensing context
  • Object counts via snapshot metrics
  • All account-level engagement scoring in Accoil

What you give up

  • Individual user journeys and funnels
  • Per-user segmentation (by role, tenure, etc.)
  • User-level cohort analysis

When to use it

Ask: "Do our product questions require knowing which user did something, or just which account?"

Use instance-level whenUse per-user when
Account-level analytics are sufficientYou need individual user journeys
High user counts per account inflate costsRole-based segmentation is critical
Privacy is a priority (fewer individual IDs)Per-user onboarding analysis matters
Public or anonymous users generate throwaway identitiesUser-level cohort analysis drives decisions

You can always switch from instance-level to per-user later if needs change. The reverse -- rolling up per-user data to account-level after the fact -- is harder.

Strategy 3: Eliminate page view tracking

Page views are the single largest source of event volume inflation, and they are almost always the lowest-value events in a tracking plan.

The math

A user navigating your app generates dozens of page views per session. With 1,000 daily active users averaging 20 page views each, that is 20,000 events per day -- 600,000 per month -- just from navigation. Meanwhile, the meaningful feature events (report created, task completed) might total 50,000 per month.

Page views often outnumber meaningful events by 10:1 or more. On volume-billed platforms, this means paying ten times more for data that rarely answers a useful product question.

What to do instead

Track feature engagement events. Report_Created tells you more about product health than fifty Page_Viewed events on the reports page.

If you need to understand navigation patterns or page-level engagement, use a dedicated tool with auto-capture (PostHog session replay, FullStory, Hotjar) rather than polluting your analytics event pipeline.

The exception

A handful of commercially significant pages may justify tracking as intent signals:

  • Pricing page viewed -- strong upgrade intent signal
  • Upgrade page viewed -- commercial intent signal

Limit to 2-3 pages maximum. These are billing-adjacent events, not navigation events.

Strategy 4: Collapse lifecycle step inflation

Track the outcome, not every intermediate state.

The problem

// Four events for one user action
track("Report_Creation_Started");
track("Report_Running");
track("Report_Run_Completed");
track("Report_Created");

If a report was created, it was obviously started and run. Four events for one action means four times the cost with zero additional insight. The completion event is the only one anyone will ever query.

The fix

Track the outcome only:

track("Report_Created");

If duration or method matters, push that data to traits or snapshot metrics rather than creating intermediate events.

When start + completion is justified

Track both start and completion only when abandonment is a meaningful signal:

  • A multi-step onboarding wizard where users drop off at step 3 -- Onboarding_Started + Onboarding_Completed
  • A lengthy configuration flow with known abandonment -- Setup_Wizard_Started + Setup_Wizard_Completed

A synchronous report generation does not justify separate start and end events. During tracking plan design, actively look for lifecycle bloat: chains of *_Started, *_In_Progress, *_Completed, *_Created that should be collapsed to the outcome event.

Strategy 5: Snapshot metrics instead of derived counts

Never try to derive current counts from events. It does not work reliably and generates unnecessary event volume.

The problem with event-derived counts

In theory: todos_created - todos_deleted = current_todo_count

In practice:

  • Events can be dropped or duplicated
  • Historical events may predate your tracking
  • Edge cases accumulate drift over time
  • Migrations and imports bypass event tracking

The fix: snapshot metrics

Query your database directly on a schedule and send current-state data as group traits:

// Daily scheduled job
async function sendDailySnapshot(accountId) {
  const traits = {
    total_projects: await db.projects.count({ accountId }),
    active_user_count: await db.users.countActive({ accountId }),
    total_integrations: await db.integrations.count({ accountId }),
    is_active: true,
    last_daily_sync: new Date().toISOString(),
  };

  group(accountId, traits);
}

Snapshots are sent via group() calls, not track calls. They do not inflate event volume. They are calculated from your database (the source of truth), not from event logs. And they include a timestamp so you always know how fresh the data is.

See Snapshot Metrics for the full pattern.

Strategy 6: Handle public and anonymous users

For products with public-facing usage (customer portals, forms, public dashboards), avoid tracking each anonymous user separately.

The problem

Thousands of public users create thousands of MTUs. On MTU-billed platforms, this inflates costs dramatically for users who will never become paying customers.

The fix

Track all public user events under a shared instance ID:

function trackPublicAction(accountId, eventName) {
  // Use account ID as user ID for all public users
  track(eventName);
  // The identify call uses the account ID
  identify(accountId, {
    is_public_user: true,
  });
}

This reduces MTUs to one per account for all public usage while maintaining visibility into what public users do.

Cost optimization checklist

Review your tracking plan against these checks:

  • Event consolidation: Are there event families that could be a single event with traits for context?
  • Page views: Are you tracking page views? Can they be removed?
  • Lifecycle bloat: Are there event chains (*_Started + *_Completed + *_Created) that could be collapsed?
  • Instance-level: Do your analytics questions require per-user data, or would account-level suffice?
  • Snapshot metrics: Are you deriving counts from events that should be snapshots?
  • Public users: Are anonymous or public users inflating your MTU count?
  • High-frequency events: Are there events that fire many times per session with diminishing value?
  • Speculative events: Are there events added "just in case" that nobody queries?

On this page