On this page

🔗The Cost Reality of LLM APIs

LLM APIs are cheap per request, but they add up fast when you're generating mock data at scale. A single mock generation might cost $0.0004, but if you're running thousands of tests per day, that's real money. More importantly, it's unpredictable money—you don't know how much you're spending until the bill arrives.

The problem isn't just the cost itself; it's the lack of visibility and control. Traditional caching strategies don't work well with LLMs because the prompts are often unique, and the outputs are non-deterministic by default. You need a different approach.

This post covers the practical patterns we've developed for keeping LLM-powered mock generation costs under control while maintaining the quality and realism that makes it valuable.

🔗Strategy 1: Aggressive Schema-Based Caching

The most important optimization is caching. But not just any caching—schema-based caching that understands when two requests are equivalent.

The key insight: two requests for mock data with the same schema structure should produce the same result (or at least, you should be able to reuse a cached result). The schema defines the structure, and if the structure is identical, the generated data can be reused.

// Cache key is schema structure, not prompt text
fn generate_cached(schema: &Value, cache: &Cache) -> Result<Value> {
    // Hash the schema structure (ignoring descriptions, examples)
    let schema_hash = hash_schema_structure(schema);
    
    // Check cache
    if let Some(cached) = cache.get(&schema_hash) {
        metrics::cache_hit();
        return Ok(cached.clone());
    }
    
    // Generate via LLM
    let generated = generate_via_llm(schema)?;
    
    // Cache the result
    cache.set(&schema_hash, &generated, TTL_30_DAYS);
    metrics::cache_miss();
    
    Ok(generated)
}

The hash function needs to normalize the schema—ignore descriptions, examples, and other metadata that doesn't affect the structure. Two schemas with identical structure but different descriptions should hash to the same value.

Cache hit rate targets: Aim for 70-90% cache hit rate. If you're below 50%, you're either generating too many unique schemas or your cache isn't configured correctly. If you're above 95%, you might be over-caching and missing valuable variation.

🔗Strategy 2: Deterministic Seeding for Reproducibility

For test reproducibility, you need deterministic data. But deterministic data doesn't mean you can't use LLMs—it means you need to seed the generation process consistently.

fn generate_with_seed(schema: &Value, seed: u64) -> Result<Value> {
    // Include seed in cache key
    let cache_key = format!("{}:{}", hash_schema_structure(schema), seed);
    
    if let Some(cached) = cache.get(&cache_key) {
        return Ok(cached.clone());
    }
    
    // Include seed in prompt for reproducibility
    let prompt = build_prompt(schema, Some(seed));
    let generated = call_llm(&prompt)?;
    
    cache.set(&cache_key, &generated, TTL_30_DAYS);
    Ok(generated)
}

The seed is included in both the prompt (to influence generation) and the cache key (to ensure same seed = same result). This gives you the best of both worlds: LLM-powered realistic data with deterministic reproducibility.

When to use seeding:

  • Unit tests that need reproducible results
  • CI/CD pipelines where flaky tests are unacceptable
  • Debugging scenarios where you need to reproduce exact conditions

When not to use seeding:

  • Exploratory testing where variation is valuable
  • Demo environments where realistic variety matters
  • Staging environments where you want to catch edge cases

🔗Strategy 3: Provider Selection and Model Tiers

Not all LLM providers are created equal, and not all use cases need GPT-4's sophistication. For mock data generation, cheaper models often work just as well.

// Provider selection based on cost/quality trade-off
enum ModelTier {
    Fast,   // GPT-3.5-turbo, Claude Haiku (~$0.001 per request)
    Balanced, // GPT-4o-mini (~$0.002 per request)
    HighQuality, // GPT-4, Claude Sonnet (~$0.01+ per request)
}

fn select_provider(schema_complexity: usize) -> ModelTier {
    match schema_complexity {
        0..=10 => ModelTier::Fast,        // Simple schemas
        11..=50 => ModelTier::Balanced,    // Medium complexity
        _ => ModelTier::HighQuality,       // Complex schemas with many relationships
    }
}

Cost comparison:

  • GPT-3.5-turbo: ~$0.001 per generation (simple schema)
  • GPT-4o-mini: ~$0.002 per generation
  • GPT-4: ~$0.01+ per generation

For most mock data generation, GPT-3.5-turbo is perfectly adequate. The quality difference between GPT-3.5 and GPT-4 for structured JSON generation is minimal, but the cost difference is 10x.

When to use higher-tier models:

  • Complex schemas with many relationships
  • Business logic constraints that require reasoning
  • When quality is more important than cost

When generating multiple related objects, batch them in a single LLM call. This reduces API calls and improves consistency.

// Instead of 3 separate calls:
let user = generate_user()?;        // 1 API call
let order = generate_order(user.id)?; // 1 API call
let items = generate_items(order.id)?; // 1 API call
// Total: 3 API calls

// Batch into 1 call:
let batch = generate_user_with_order_and_items()?; // 1 API call
// Total: 1 API call, better relationships

Batching has two benefits:

  1. Cost reduction: 1 API call instead of N calls
  2. Better relationships: LLM can maintain consistency across the batch

The prompt for batch generation includes all schemas and relationship constraints:

Generate a user object with their first order and order items.

User schema: {...}
Order schema: {...}
OrderItem schema: {...}

Requirements:
- Order.userId must match User.id
- OrderItem.orderId must match Order.id
- OrderItem.productId must reference valid products
- Order.total must equal sum of OrderItem prices
- Timestamps must be chronologically consistent

🔗Strategy 5: Prompt Optimization

The prompt is what you pay for (input tokens) and what you get back (output tokens). Optimizing the prompt reduces both costs.

What to include:

  • Schema structure (required)
  • Field constraints (required)
  • Relationship hints (valuable)
  • Format requirements (required)

What to exclude:

  • Verbose descriptions (unless necessary)
  • Redundant examples (unless they clarify constraints)
  • Unnecessary context (unless it improves quality)

Here's an optimized prompt:

Generate JSON for User schema:
{
  "id": "uuid (required)",
  "email": "email (required)",
  "name": "string (required)",
  "createdAt": "iso8601 (required)"
}
Requirements: realistic data, valid formats, required fields.
Return JSON only.

Compare that to a verbose prompt with unnecessary context—the optimized version is 50% shorter, which means 50% lower input token costs.

🔗Strategy 6: Request Deduplication

Before calling the LLM, check if you've already generated data for this exact request recently. This is different from schema-based caching—this is request-level deduplication.

fn generate_with_deduplication(request_id: &str, schema: &Value) -> Result<Value> {
    // Check if we've already processed this exact request
    if let Some(cached) = request_cache.get(request_id) {
        return Ok(cached.clone());
    }
    
    // Otherwise, use schema-based cache
    let generated = generate_cached(schema, &schema_cache)?;
    
    // Cache at request level too
    request_cache.set(request_id, &generated, TTL_5_MINUTES);
    
    Ok(generated)
}

This is especially valuable for:

  • Retry scenarios (don't regenerate on retry)
  • Concurrent requests (don't generate the same data twice)
  • Test suites that run the same tests multiple times

🔗Strategy 7: Cost Budgets and Circuit Breakers

Set hard limits on LLM spending. When the budget is exceeded, fall back to traditional faker libraries.

struct CostTracker {
    daily_budget: f64,
    daily_spent: Arc<AtomicU64>, // in micro-dollars
}

fn generate_with_budget(schema: &Value, tracker: &CostTracker) -> Result<Value> {
    // Check budget
    let spent = tracker.daily_spent.load(Ordering::Relaxed) as f64 / 1_000_000.0;
    if spent >= tracker.daily_budget {
        // Fall back to faker
        warn!("Budget exceeded, falling back to faker");
        return Ok(generate_with_faker(schema));
    }
    
    // Estimate cost
    let estimated_cost = estimate_generation_cost(schema);
    
    // Check if we can afford it
    if spent + estimated_cost > tracker.daily_budget {
        return Ok(generate_with_faker(schema));
    }
    
    // Generate and track cost
    let generated = generate_via_llm(schema)?;
    let actual_cost = calculate_actual_cost(/* tokens used */);
    tracker.daily_spent.fetch_add((actual_cost * 1_000_000.0) as u64, Ordering::Relaxed);
    
    Ok(generated)
}

This ensures you never exceed your budget, even if caching fails or there's a bug in your cost estimation.

🔗Real-World Cost Analysis

Let's model a realistic scenario:

Setup:

  • 1,000 test runs per day
  • Average 5 mock generations per test
  • Total: 5,000 generations per day
  • Cache hit rate: 80%
  • Actual LLM calls: 1,000 per day

Costs:

  • GPT-3.5-turbo: ~$0.001 per generation
  • Daily cost: $1.00
  • Monthly cost: ~$30

With optimizations:

  • Better caching (90% hit rate): $0.50/day, $15/month
  • Model selection (GPT-3.5 vs GPT-4): 10x savings
  • Batching (50% reduction): $0.25/day, $7.50/month

The difference between unoptimized and optimized is 4x cost reduction. For a team running 10,000 tests per day, that's $300/month vs $75/month.

🔗Observability: Tracking What Matters

You can't optimize what you can't measure. Track:

Cost metrics:

  • Tokens consumed (input + output) per generation
  • Cost per generation
  • Daily/weekly/monthly spend
  • Cost per test run

Efficiency metrics:

  • Cache hit rate (target: >70%)
  • Average generations per test
  • Provider distribution (which models are being used)
  • Batch utilization rate

Quality metrics:

  • Generation success rate
  • Schema validation pass rate
  • Relationship consistency rate

Set up alerts for:

  • Daily spend exceeding budget
  • Cache hit rate dropping below 50%
  • Unusual cost spikes
  • Generation failures

🔗The Bottom Line

LLM-powered mock generation doesn't have to be expensive. With the right strategies—aggressive caching, deterministic seeding, provider selection, batching, and cost budgets—you can keep costs under $0.01 per 1,000 requests.

The key is treating it like any other resource: measure it, optimize it, and set limits. Don't let the "AI magic" make you forget that this is still a cost that needs to be managed.

For most teams, the cost of LLM-powered mocks is negligible compared to the value of more realistic test data. But without proper optimization, costs can spiral. These patterns ensure you get the benefits without the budget surprises.