Skip to main content

Performance Optimization

1. Use Batch Endpoints

Always use the multi-odds endpoint when fetching odds for multiple events: Bad:
// ❌ Makes 10 API requests
for (const eventId of eventIds) {
  const odds = await fetchOdds(eventId);
}
Good:
// ✅ Makes 1 API request
const eventIds = '123456,123457,123458'; // Up to 10 events
const odds = await fetchMultiOdds(eventIds);

2. Implement Caching

Cache responses based on data type and freshness requirements:
const cache = new Map();

function getCacheKey(endpoint, params) {
  return `${endpoint}:${JSON.stringify(params)}`;
}

async function cachedFetch(endpoint, params, ttl = 60000) {
  const key = getCacheKey(endpoint, params);
  const cached = cache.get(key);

  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data;
  }

  const data = await fetch(endpoint, params);
  cache.set(key, { data, timestamp: Date.now() });

  return data;
}

// Usage
const sports = await cachedFetch('/sports', {}, 3600000); // Cache for 1 hour
const events = await cachedFetch('/events', { sport: 'football' }, 300000); // 5 minutes
const odds = await cachedFetch('/odds', { eventId: 123 }, 30000); // 30 seconds
Recommended Cache TTLs:
Data TypeTTLReason
Sports list1 hour+Rarely changes
Leagues list1 hourRarely changes
Pre-match events5-10 minutesUpdated periodically
Pre-match odds30-60 secondsChanges frequently
Live events10-30 secondsChanges very frequently
Live odds5-10 secondsChanges in real-time

3. Select Bookmakers Wisely

Don’t fetch odds from all 250+ bookmakers:
// ✅ Select relevant bookmakers for your region
const popularEUBookmakers = [
  'Bet365',
  'Unibet',
  'William Hill',
  'Betway',
  'Bwin'
];

const popularUSBookmakers = [
  'FanDuel',
  'DraftKings',
  'BetMGM',
  'Caesars'
];

// Fetch odds only from relevant bookmakers
const odds = await fetchOdds(eventId, popularEUBookmakers.join(','));

4. Filter Events by Bookmaker

When you only care about events available at specific bookmakers, use the bookmaker parameter on the events endpoint. This returns only events that have odds from that bookmaker:
// ✅ Only get events with Bet365 odds
const response = await fetch(
  `https://api.odds-api.io/v3/events?apiKey=${apiKey}&sport=football&bookmaker=Bet365`
);

const events = await response.json();
// All returned events are guaranteed to have Bet365 odds
This is much more efficient than fetching all events and then filtering client-side. It reduces response size and ensures you only process relevant events.

Rate Limit Management

1. Implement Rate Limiting

Track and respect API rate limits:
class RateLimiter {
  constructor(requestsPerMinute) {
    this.limit = requestsPerMinute;
    this.requests = [];
  }

  async waitForSlot() {
    const now = Date.now();
    const minute = 60 * 1000;

    // Remove requests older than 1 minute
    this.requests = this.requests.filter(time => now - time < minute);

    // Wait if at limit
    if (this.requests.length >= this.limit) {
      const oldestRequest = this.requests[0];
      const waitTime = minute - (now - oldestRequest);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      return this.waitForSlot();
    }

    this.requests.push(now);
  }

  async execute(fn) {
    await this.waitForSlot();
    return fn();
  }
}

// Usage
const limiter = new RateLimiter(30); // 30 requests per minute

async function fetchOdds(eventId) {
  return limiter.execute(() =>
    fetch(`https://api.odds-api.io/v3/odds?apiKey=${apiKey}&eventId=${eventId}`)
  );
}

2. Handle 429 Responses

Implement exponential backoff for rate limit errors:
async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url);

      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After');
        const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, i) * 1000;

        console.log(`Rate limited. Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return response.json();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
  }
}

3. Monitor Usage

Track your API usage to avoid hitting limits:
class UsageTracker {
  constructor() {
    this.requestCount = 0;
    this.startTime = Date.now();
  }

  recordRequest() {
    this.requestCount++;
  }

  getStats() {
    const elapsed = (Date.now() - this.startTime) / 1000 / 60; // minutes
    const requestsPerMinute = this.requestCount / elapsed;

    return {
      totalRequests: this.requestCount,
      elapsedMinutes: elapsed.toFixed(2),
      requestsPerMinute: requestsPerMinute.toFixed(2)
    };
  }

  reset() {
    this.requestCount = 0;
    this.startTime = Date.now();
  }
}

const tracker = new UsageTracker();

async function monitoredFetch(url) {
  tracker.recordRequest();
  const response = await fetch(url);

  // Log usage every 100 requests
  if (tracker.requestCount % 100 === 0) {
    console.log('Usage stats:', tracker.getStats());
  }

  return response.json();
}

Error Handling

1. Graceful Degradation

Handle missing data gracefully:
function getBestOdds(oddsData, market = 'ML') {
  try {
    const bookmakerOdds = Object.entries(oddsData.bookmakers)
      .map(([bookmaker, markets]) => {
        const targetMarket = markets.find(m => m.name === market);
        if (!targetMarket?.odds?.[0]) return null;

        return {
          bookmaker,
          ...targetMarket.odds[0]
        };
      })
      .filter(Boolean);

    if (bookmakerOdds.length === 0) {
      return null; // No odds available
    }

    // Find best odds for each outcome
    return {
      home: bookmakerOdds.reduce((best, current) =>
        parseFloat(current.home || 0) > parseFloat(best.home || 0) ? current : best
      ),
      away: bookmakerOdds.reduce((best, current) =>
        parseFloat(current.away || 0) > parseFloat(best.away || 0) ? current : best
      )
    };
  } catch (error) {
    console.error('Error finding best odds:', error);
    return null;
  }
}

2. Validate Responses

Always validate API responses:
function validateOddsResponse(data) {
  if (!data || typeof data !== 'object') {
    throw new Error('Invalid response format');
  }

  if (!data.id || !data.home || !data.away) {
    throw new Error('Missing required fields');
  }

  if (!data.bookmakers || typeof data.bookmakers !== 'object') {
    throw new Error('Invalid bookmakers data');
  }

  return true;
}

async function fetchOdds(eventId) {
  try {
    const response = await fetch(
      `https://api.odds-api.io/v3/odds?apiKey=${apiKey}&eventId=${eventId}&bookmakers=Bet365`
    );

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();
    validateOddsResponse(data);

    return data;
  } catch (error) {
    console.error('Failed to fetch odds:', error);
    throw error;
  }
}

3. Timeout Handling

Set timeouts for API requests:
async function fetchWithTimeout(url, timeout = 10000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    clearTimeout(id);
    return response;
  } catch (error) {
    clearTimeout(id);
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw error;
  }
}

Security Best Practices

1. Secure API Keys

Never expose API keys in client-side code!
// ❌ Bad - API key exposed in browser
const apiKey = 'sk_live_1234567890';
fetch(`https://api.odds-api.io/v3/odds?apiKey=${apiKey}`);

// ✅ Good - API key stays on server
// Frontend
fetch('/api/proxy/odds?eventId=123456');

// Backend
app.get('/api/proxy/odds', async (req, res) => {
  const apiKey = process.env.ODDS_API_KEY;
  const response = await fetch(
    `https://api.odds-api.io/v3/odds?apiKey=${apiKey}&eventId=${req.query.eventId}`
  );
  const data = await response.json();
  res.json(data);
});

2. Validate Input

Always validate user input before using it in API calls:
function validateEventId(eventId) {
  const id = parseInt(eventId);
  if (isNaN(id) || id <= 0) {
    throw new Error('Invalid event ID');
  }
  return id;
}

function validateBookmakers(bookmakers) {
  const validBookmakers = bookmakers.split(',')
    .map(b => b.trim())
    .filter(b => b.length > 0);

  if (validBookmakers.length === 0 || validBookmakers.length > 30) {
    throw new Error('Invalid bookmakers list (1-30 required)');
  }

  return validBookmakers.join(',');
}

// Usage
app.get('/api/odds', async (req, res) => {
  try {
    const eventId = validateEventId(req.query.eventId);
    const bookmakers = validateBookmakers(req.query.bookmakers);

    const data = await fetchOdds(eventId, bookmakers);
    res.json(data);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Data Management

1. Database Schema

Store odds data efficiently:
-- Events table
CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  sport VARCHAR(50),
  league VARCHAR(100),
  home_team VARCHAR(100),
  away_team VARCHAR(100),
  event_date TIMESTAMP,
  status VARCHAR(20),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Odds table (time-series data)
CREATE TABLE odds (
  id SERIAL PRIMARY KEY,
  event_id INTEGER REFERENCES events(id),
  bookmaker VARCHAR(50),
  market VARCHAR(50),
  home_odds DECIMAL(10, 2),
  draw_odds DECIMAL(10, 2),
  away_odds DECIMAL(10, 2),
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_event_time (event_id, timestamp)
);

-- Optimize for time-series queries
CREATE INDEX idx_odds_timestamp ON odds(timestamp DESC);
CREATE INDEX idx_odds_event_bookmaker ON odds(event_id, bookmaker, market);

2. Store Historical Data

Keep historical odds for analysis:
async function storeOdds(eventId, oddsData) {
  const timestamp = new Date();

  for (const [bookmaker, markets] of Object.entries(oddsData.bookmakers)) {
    for (const market of markets) {
      if (!market.odds[0]) continue;

      await db.query(
        `INSERT INTO odds (event_id, bookmaker, market, home_odds, draw_odds, away_odds, timestamp)
         VALUES ($1, $2, $3, $4, $5, $6, $7)`,
        [
          eventId,
          bookmaker,
          market.name,
          market.odds[0].home,
          market.odds[0].draw,
          market.odds[0].away,
          timestamp
        ]
      );
    }
  }
}

3. Cleanup Old Data

Regularly clean up stale data:
// Delete odds older than 90 days
async function cleanupOldOdds() {
  await db.query(
    `DELETE FROM odds WHERE timestamp < NOW() - INTERVAL '90 days'`
  );
}

// Run daily cleanup
setInterval(cleanupOldOdds, 24 * 60 * 60 * 1000);

Monitoring & Logging

1. Log Important Events

class APILogger {
  log(level, message, meta = {}) {
    const entry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      ...meta
    };

    console.log(JSON.stringify(entry));

    // Send to logging service (e.g., Winston, Pino, CloudWatch)
  }

  info(message, meta) {
    this.log('info', message, meta);
  }

  error(message, error, meta) {
    this.log('error', message, {
      ...meta,
      error: error.message,
      stack: error.stack
    });
  }

  metric(name, value, unit = 'count') {
    this.log('metric', name, { value, unit });
  }
}

const logger = new APILogger();

// Usage
async function fetchOdds(eventId) {
  const startTime = Date.now();

  try {
    logger.info('Fetching odds', { eventId });

    const data = await fetch(`...`);

    const duration = Date.now() - startTime;
    logger.metric('odds_fetch_duration', duration, 'ms');

    return data;
  } catch (error) {
    logger.error('Failed to fetch odds', error, { eventId });
    throw error;
  }
}

2. Set Up Alerts

Monitor critical metrics:
class AlertSystem {
  constructor(thresholds) {
    this.thresholds = thresholds;
    this.metrics = {};
  }

  recordMetric(name, value) {
    if (!this.metrics[name]) {
      this.metrics[name] = [];
    }

    this.metrics[name].push({
      value,
      timestamp: Date.now()
    });

    // Keep only last 100 values
    if (this.metrics[name].length > 100) {
      this.metrics[name].shift();
    }

    this.checkThreshold(name);
  }

  checkThreshold(name) {
    const threshold = this.thresholds[name];
    if (!threshold) return;

    const recent = this.metrics[name].slice(-10);
    const average = recent.reduce((sum, m) => sum + m.value, 0) / recent.length;

    if (average > threshold) {
      this.sendAlert(name, average, threshold);
    }
  }

  sendAlert(metric, value, threshold) {
    console.error(`ALERT: ${metric} is ${value}, threshold: ${threshold}`);
    // Send to alerting service (PagerDuty, Slack, etc.)
  }
}

const alerts = new AlertSystem({
  error_rate: 0.05, // 5% error rate
  response_time: 2000, // 2 second response time
  rate_limit_hits: 5 // 5 rate limit hits in 10 requests
});

Testing

1. Unit Tests

const { describe, it, expect } = require('@jest/globals');

describe('Odds API Client', () => {
  it('should fetch odds for an event', async () => {
    const eventId = 123456;
    const odds = await fetchOdds(eventId);

    expect(odds).toBeDefined();
    expect(odds.id).toBe(eventId);
    expect(odds.bookmakers).toBeDefined();
  });

  it('should handle invalid event ID', async () => {
    await expect(fetchOdds('invalid')).rejects.toThrow();
  });

  it('should cache responses', async () => {
    const eventId = 123456;

    const start1 = Date.now();
    await getCachedOdds(eventId);
    const time1 = Date.now() - start1;

    const start2 = Date.now();
    await getCachedOdds(eventId);
    const time2 = Date.now() - start2;

    expect(time2).toBeLessThan(time1);
  });
});

2. Integration Tests

describe('Odds API Integration', () => {
  it('should fetch and compare odds', async () => {
    // Fetch events
    const events = await fetchEvents('football');
    expect(events.length).toBeGreaterThan(0);

    // Fetch odds for first event
    const odds = await fetchOdds(events[0].id);
    expect(odds.bookmakers).toBeDefined();

    // Find best odds
    const bestOdds = findBestOdds(odds);
    expect(bestOdds.home).toBeDefined();
    expect(bestOdds.away).toBeDefined();
  });
});

Summary Checklist

  • ✅ Use batch endpoints when possible
  • ✅ Implement appropriate caching
  • ✅ Select relevant bookmakers only
  • ✅ Minimize unnecessary API calls
  • ✅ Implement rate limiting logic
  • ✅ Handle 429 responses with backoff
  • ✅ Monitor and track usage
  • ✅ Use multi-endpoints efficiently
  • ✅ Validate all responses
  • ✅ Handle missing data gracefully
  • ✅ Implement request timeouts
  • ✅ Log errors properly
  • ✅ Never expose API keys client-side
  • ✅ Use environment variables
  • ✅ Validate all user input
  • ✅ Implement server-side proxy
  • ✅ Design efficient database schema
  • ✅ Store historical data when needed
  • ✅ Clean up old data regularly
  • ✅ Index time-series queries