Skip to main content
Transports control how the SDK sends events to Sentry. You can create custom transports to route events through proxies, add custom headers, implement retry logic, or integrate with custom infrastructure.

Transport Basics

A transport is responsible for sending envelopes (containing events, transactions, etc.) to Sentry:
import { createTransport } from '@sentry/core';

function makeCustomTransport(options) {
  return createTransport(options, async ({ body, headers }) => {
    // Send the envelope
    const response = await fetch(options.url, {
      method: 'POST',
      headers,
      body,
    });
    
    return {
      statusCode: response.status,
      headers: {
        'x-sentry-rate-limits': response.headers.get('x-sentry-rate-limits'),
        'retry-after': response.headers.get('retry-after'),
      },
    };
  });
}

export { makeCustomTransport };

Using Custom Transports

import * as Sentry from '@sentry/browser';
import { makeCustomTransport } from './transports/custom';

Sentry.init({
  dsn: '__DSN__',
  transport: makeCustomTransport,
});

Built-in Transports

The SDK provides standard transports:

Browser

import { makeFetchTransport } from '@sentry/browser';

Sentry.init({
  dsn: '__DSN__',
  transport: makeFetchTransport, // Default
});

Node.js

import { makeNodeTransport } from '@sentry/node';

Sentry.init({
  dsn: '__DSN__',
  transport: makeNodeTransport, // Default
});

Custom Transport Examples

Proxy Transport

Route events through a custom proxy:
import { createTransport } from '@sentry/core';

function makeProxyTransport(options) {
  const proxyUrl = options.proxy || '/api/sentry';
  
  return createTransport(options, async ({ body }) => {
    const response = await fetch(proxyUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-sentry-envelope',
      },
      body,
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return {
      statusCode: response.status,
    };
  });
}

export { makeProxyTransport };
Usage:
Sentry.init({
  dsn: '__DSN__',
  transport: makeProxyTransport,
  transportOptions: {
    proxy: '/api/sentry-proxy',
  },
});

Batching Transport

Batch multiple envelopes together:
import { createTransport } from '@sentry/core';

function makeBatchingTransport(options) {
  const queue: any[] = [];
  const batchSize = options.batchSize || 5;
  const flushInterval = options.flushInterval || 5000;
  let flushTimer: NodeJS.Timeout | null = null;
  
  function flush() {
    if (queue.length === 0) return;
    
    const batch = queue.splice(0, queue.length);
    
    // Send batch
    fetch(options.url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(batch),
    }).catch(error => {
      console.error('Failed to send batch:', error);
    });
  }
  
  return createTransport(options, async ({ body }) => {
    queue.push(body);
    
    // Flush if batch size reached
    if (queue.length >= batchSize) {
      if (flushTimer) clearTimeout(flushTimer);
      flush();
    } else {
      // Schedule flush
      if (flushTimer) clearTimeout(flushTimer);
      flushTimer = setTimeout(flush, flushInterval);
    }
    
    return { statusCode: 200 };
  });
}

export { makeBatchingTransport };

Logging Transport

Log events instead of sending them (useful for testing):
import { createTransport } from '@sentry/core';

function makeLoggingTransport(options) {
  return createTransport(options, async ({ body, headers }) => {
    console.log('--- Sentry Event ---');
    console.log('Headers:', headers);
    console.log('Body:', body);
    console.log('---');
    
    return {
      statusCode: 200,
    };
  });
}

export { makeLoggingTransport };

Fallback Transport

Try multiple endpoints with fallback:
import { createTransport } from '@sentry/core';

function makeFallbackTransport(options) {
  const endpoints = options.endpoints || [options.url];
  
  return createTransport(options, async (transportOptions) => {
    let lastError;
    
    for (const endpoint of endpoints) {
      try {
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: transportOptions.headers,
          body: transportOptions.body,
        });
        
        if (response.ok) {
          return {
            statusCode: response.status,
          };
        }
      } catch (error) {
        lastError = error;
        continue; // Try next endpoint
      }
    }
    
    throw lastError || new Error('All endpoints failed');
  });
}

export { makeFallbackTransport };

Transport with Custom Headers

Add authentication or custom headers:
import { createTransport } from '@sentry/core';

function makeAuthTransport(options) {
  return createTransport(options, async ({ body, headers }) => {
    const response = await fetch(options.url, {
      method: 'POST',
      headers: {
        ...headers,
        'Authorization': `Bearer ${options.apiKey}`,
        'X-Custom-Header': 'custom-value',
      },
      body,
    });
    
    return {
      statusCode: response.status,
    };
  });
}

export { makeAuthTransport };
Usage:
Sentry.init({
  dsn: '__DSN__',
  transport: makeAuthTransport,
  transportOptions: {
    apiKey: process.env.API_KEY,
  },
});

Offline Transport

Queue events when offline:
import { makeBrowserOfflineTransport } from '@sentry/browser';
import { makeFetchTransport } from '@sentry/browser';

Sentry.init({
  dsn: '__DSN__',
  transport: makeBrowserOfflineTransport(makeFetchTransport),
});
Custom offline implementation:
import { createTransport } from '@sentry/core';

function makeOfflineTransport(baseTransport) {
  return function(options) {
    const base = baseTransport(options);
    const queue: any[] = [];
    
    // Check connectivity
    const isOnline = () => navigator.onLine;
    
    // Flush queue when online
    window.addEventListener('online', () => {
      queue.forEach(item => base.send(item));
      queue.length = 0;
    });
    
    return createTransport(options, async (transportOptions) => {
      if (!isOnline()) {
        queue.push(transportOptions);
        return { statusCode: 200 }; // Queued
      }
      
      return base.send(transportOptions);
    });
  };
}

export { makeOfflineTransport };

Multiplexed Transport

Send to multiple Sentry projects:
import {
  makeMultiplexedTransport,
  MULTIPLEXED_TRANSPORT_EXTRA_KEY,
} from '@sentry/browser';

Sentry.init({
  dsn: '__DSN__', // Default DSN
  transport: makeMultiplexedTransport(
    makeFetchTransport,
    (args) => {
      const event = args.getEvent();
      
      // Route to different projects based on event type
      if (event && event.type === 'transaction') {
        return [
          { dsn: 'https://[email protected]/project', release: 'v1.0' },
        ];
      }
      
      // Default: use main DSN
      return [];
    }
  ),
});

// Tag events for specific projects
Sentry.captureException(error, (scope) => {
  scope.setExtra(MULTIPLEXED_TRANSPORT_EXTRA_KEY, [
    { dsn: 'https://[email protected]/project' },
  ]);
});

Rate Limiting

Handle rate limits properly:
import { createTransport } from '@sentry/core';

function makeRateLimitedTransport(options) {
  let rateLimitedUntil: number | null = null;
  
  return createTransport(options, async (transportOptions) => {
    // Check if rate limited
    if (rateLimitedUntil && Date.now() < rateLimitedUntil) {
      console.warn('Rate limited, dropping event');
      return { statusCode: 429 };
    }
    
    const response = await fetch(options.url, {
      method: 'POST',
      headers: transportOptions.headers,
      body: transportOptions.body,
    });
    
    // Handle rate limit headers
    const retryAfter = response.headers.get('retry-after');
    if (retryAfter) {
      rateLimitedUntil = Date.now() + (parseInt(retryAfter, 10) * 1000);
    }
    
    return {
      statusCode: response.status,
      headers: {
        'retry-after': retryAfter,
      },
    };
  });
}

export { makeRateLimitedTransport };

Testing Transports

import { describe, it, expect, vi } from 'vitest';
import { makeCustomTransport } from './custom-transport';

describe('CustomTransport', () => {
  it('should send events', async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      headers: new Headers(),
    });
    
    global.fetch = mockFetch;
    
    const transport = makeCustomTransport({
      url: 'https://sentry.io/api/envelope',
    });
    
    const result = await transport.send({
      body: 'test-envelope',
      headers: {},
    });
    
    expect(result.statusCode).toBe(200);
    expect(mockFetch).toHaveBeenCalledWith(
      'https://sentry.io/api/envelope',
      expect.objectContaining({
        method: 'POST',
        body: 'test-envelope',
      })
    );
  });
});

Best Practices

Don’t let transport errors crash the app:
return createTransport(options, async (transportOptions) => {
  try {
    const response = await fetch(/* ... */);
    return { statusCode: response.status };
  } catch (error) {
    console.error('Transport error:', error);
    return { statusCode: 500 }; // Failed
  }
});
Honor Sentry’s rate limit headers:
const retryAfter = response.headers.get('retry-after');
if (retryAfter) {
  // Wait before sending more events
}
Prevent hanging requests:
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);

try {
  const response = await fetch(url, {
    signal: controller.signal,
    // ...
  });
} finally {
  clearTimeout(timeoutId);
}
Retry failed requests with exponential backoff:
async function sendWithRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
    }
  }
}

Complete Example

import { createTransport } from '@sentry/core';

interface CustomTransportOptions {
  url: string;
  proxy?: string;
  apiKey?: string;
  timeout?: number;
  retries?: number;
}

function makeProductionTransport(options: CustomTransportOptions) {
  const {
    url,
    proxy,
    apiKey,
    timeout = 30000,
    retries = 3,
  } = options;
  
  const endpoint = proxy || url;
  
  async function sendWithRetry(
    transportOptions: any,
    retriesLeft: number
  ): Promise<any> {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
          ...transportOptions.headers,
          ...(apiKey && { 'Authorization': `Bearer ${apiKey}` }),
        },
        body: transportOptions.body,
        signal: controller.signal,
      });
      
      clearTimeout(timeoutId);
      
      // Check for rate limits
      const rateLimits = response.headers.get('x-sentry-rate-limits');
      const retryAfter = response.headers.get('retry-after');
      
      if (response.status === 429 && retriesLeft > 0) {
        const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        return sendWithRetry(transportOptions, retriesLeft - 1);
      }
      
      if (!response.ok && retriesLeft > 0) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return sendWithRetry(transportOptions, retriesLeft - 1);
      }
      
      return {
        statusCode: response.status,
        headers: {
          'x-sentry-rate-limits': rateLimits,
          'retry-after': retryAfter,
        },
      };
    } catch (error) {
      clearTimeout(timeoutId);
      
      if (retriesLeft > 0) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return sendWithRetry(transportOptions, retriesLeft - 1);
      }
      
      throw error;
    }
  }
  
  return createTransport(options, async (transportOptions) => {
    return sendWithRetry(transportOptions, retries);
  });
}

export { makeProductionTransport };
Usage:
import * as Sentry from '@sentry/browser';
import { makeProductionTransport } from './transports/production';

Sentry.init({
  dsn: '__DSN__',
  transport: makeProductionTransport,
  transportOptions: {
    proxy: '/api/sentry',
    apiKey: process.env.SENTRY_API_KEY,
    timeout: 10000,
    retries: 3,
  },
});

Next Steps

Tunneling

Set up event tunneling through your backend

Custom Integrations

Build custom integrations

Build docs developers (and LLMs) love