Skip to content

Best Practices

Optimize your Enrich.sh integration for maximum performance, reliability, and cost efficiency.

Performance

1. Batch Your Events ⚡

Single-event requests are inefficient. Always batch events together.

ApproachRequests/secEvents/secEfficiency
1 event per request200200❌ Poor
10 events per request2002,000⚠️ OK
50–100 events per request20010,000–20,000✅ Optimal

Using the SDK (batching is automatic):

javascript
import { Enrich } from 'enrich.sh'

const enrich = new Enrich('sk_live_your_key')

// Just call track() — batching and flush timing are handled for you
enrich.track('events', { event: 'page_view', url: '/home' })
enrich.track('events', { event: 'click', element: 'signup_btn' })

Custom batching (without SDK):

javascript
const buffer = []

function track(event) {
  buffer.push({ ...event, ts: Date.now() })
  if (buffer.length >= 100) flush()
}

async function flush() {
  if (buffer.length === 0) return
  const events = buffer.splice(0, buffer.length)

  await fetch('https://enrich.sh/ingest', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ stream_id: 'events', data: events }),
  })
}

// Flush periodically and before page unload
setInterval(flush, 5000)
window.addEventListener('beforeunload', flush)

2. Keep Payloads Lean

Only send data you need. Every byte costs storage and processing.

javascript
// ❌ Bad: Sending entire DOM
{ event: 'click', page_html: '<!DOCTYPE html>...' }

// ✅ Good: Send only relevant data
{ event: 'click', element_id: 'signup_btn', page: '/pricing' }

Schema Strategy

1. Choose the Right Schema Mode

ScenarioModeWhy
Prototyping / getting startedflexAccept everything, iterate fast
SaaS API / ERP integrationevolveDetect upstream schema changes automatically
Financial data / compliancestrictReject anything that doesn't match
ML inference loggingevolveSchemas grow as models evolve
IoT sensor dataevolveNew device types add fields

TIP

Start with flex or evolve during development. Switch to strict for production pipelines where data contracts matter.

2. Monitor Schema Changes

When using evolve mode, check the Observability → Schema Events page regularly for:

  • New fields — a source started sending data you weren't expecting
  • Missing fields — a source stopped sending required data
  • Type changes — a field changed from integer to string (common with ERP exports)
bash
# Check schema events via API
curl "https://enrich.sh/streams/events/schema-events?days=7" \
  -H "Authorization: Bearer sk_live_your_key"

3. Use the Dead Letter Queue

With strict mode, rejected events go to the DLQ — nothing is lost. Review DLQ events to:

  • Fix upstream data issues
  • Update your schema to accept new fields
  • Reingest corrected events
bash
# Check DLQ events
curl "https://enrich.sh/streams/transactions/dlq?days=7" \
  -H "Authorization: Bearer sk_live_your_key"

Reliability

1. Handle Errors Gracefully

Implement retry logic with exponential backoff:

javascript
async function sendWithRetry(events, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('https://enrich.sh/ingest', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ stream_id: 'events', data: events }),
      })

      if (response.ok) return await response.json()

      // Don't retry client errors (4xx)
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`)
      }

      // Retry server errors (5xx)
      if (attempt < maxRetries) {
        await new Promise(r => setTimeout(r, 2 ** attempt * 1000))
      }
    } catch (err) {
      if (attempt === maxRetries) throw err
    }
  }
}

TIP

The SDK handles retries automatically.

2. Use Local Buffering

For mobile apps or offline-first clients, persist events locally:

javascript
function trackEvent(event) {
  const buffer = JSON.parse(localStorage.getItem('event_buffer') || '[]')
  buffer.push({ ...event, ts: Date.now() })
  localStorage.setItem('event_buffer', JSON.stringify(buffer))

  if (buffer.length >= 100) flushToServer()
}

async function flushToServer() {
  const buffer = JSON.parse(localStorage.getItem('event_buffer') || '[]')
  if (buffer.length === 0) return

  try {
    await sendWithRetry(buffer)
    localStorage.removeItem('event_buffer') // Clear only on success
  } catch (err) {
    console.error('Failed to flush, will retry later:', err)
  }
}

3. Use beacon() for Page Unload

The SDK's beacon() method uses fetch({ keepalive: true }) to survive page unloads:

javascript
window.addEventListener('beforeunload', () => {
  enrich.beacon() // fire-and-forget, survives tab close
})

Security

1. Never Expose API Keys Client-Side

javascript
// ❌ NEVER do this in browser code
const API_KEY = 'sk_live_abc123...'

// ✅ Use a proxy endpoint
fetch('/api/track', { body: JSON.stringify(events) })
// Your server forwards to Enrich.sh with the secret key

2. Use Separate Keys

EnvironmentKey PrefixUsage
Developmentsk_test_Local testing
Stagingsk_test_Pre-production
Productionsk_live_Real data

Data Quality

1. Use Consistent Field Names

javascript
// ❌ Inconsistent naming
{ eventType: 'click', EventTime: 123, user_ID: 'abc' }

// ✅ Consistent snake_case
{ event_type: 'click', event_time: 123, user_id: 'abc' }

2. Always Include Timestamps

javascript
{
  event: 'purchase',
  amount: 99.99,
  ts: Date.now()           // Unix timestamp (milliseconds)
}

3. Use Enrichment Templates

Let Enrich.sh add consistent metadata automatically:

bash
curl -X POST https://enrich.sh/streams \
  -H "Authorization: Bearer sk_live_your_key" \
  -d '{ "stream_id": "clicks", "template": "clickstream" }'

4. Define Field Types

Even in flex mode, defining field types improves query performance:

json
{
  "stream_id": "events",
  "schema_mode": "flex",
  "fields": {
    "ts": { "name": "timestamp", "type": "int64" },
    "amount": { "type": "float64" },
    "active": { "type": "boolean" }
  }
}

Without field types, everything is stored as string. With types, DuckDB and ClickHouse can read the data more efficiently.


Checklist

  • [ ] Batch events (50–100 per request, or use SDK auto-batching)
  • [ ] Choose a schema mode (flexevolvestrict as you mature)
  • [ ] Monitor schema events in the Observability UI
  • [ ] Review DLQ regularly if using strict mode
  • [ ] Implement retry logic with exponential backoff
  • [ ] Use local buffering for unreliable connections
  • [ ] Keep payloads lean (only send what you need)
  • [ ] Never expose API keys in client-side code
  • [ ] Use consistent field naming (snake_case)
  • [ ] Always include timestamps
  • [ ] Define field types for better query performance
  • [ ] Connect your warehouse via Dashboard → Connect

Serverless data ingestion for developers.