You can add AI image generation to any application for about $0.01 per image. This tutorial walks you through building a complete image generation system in Python, from a single API call to a production-ready service with caching, rate limiting, and cost controls.
We will use fal.ai as our provider because it offers the best price-to-quality ratio at $0.01 to $0.04 per image with FLUX models. For a full comparison of all providers and their pricing, see my image generation API pricing guide. If you prefer running models on your own hardware instead of using an API, check my guide to running FLUX locally or the FLUX vs Stable Diffusion comparison.
Prerequisites
- Python 3.10+
- A fal.ai account (free tier gives you $10 in credits to start)
- Basic familiarity with async Python and REST APIs
Step 1: Basic Setup and First Image
Install the fal.ai client:
pip install fal-client pillow fastapi uvicorn redis aiohttp
Set your API key:
export FAL_KEY="your-api-key-here"
Generate your first image:
import fal_client
import base64
from pathlib import Path
def generate_image(prompt: str, output_path: str = "output.png") -> str:
"""Generate a single image from a text prompt."""
result = fal_client.subscribe(
"fal-ai/flux/dev",
arguments={
"prompt": prompt,
"image_size": "landscape_16_9",
"num_inference_steps": 20,
"guidance_scale": 3.5,
"num_images": 1,
"enable_safety_checker": True,
},
)
image_url = result["images"][0]["url"]
print(f"Generated: {image_url}")
return image_url
# Generate a test image
url = generate_image("A modern laptop on a minimal desk, soft morning light, photorealistic")
print(f"Image URL: {url}")
Cost for this single call: approximately $0.03 (FLUX dev, 20 steps, 1024x1024).
For even cheaper generation, use FLUX schnell (4 steps) at $0.01 per image:
def generate_fast(prompt: str) -> str:
"""Generate a quick image using FLUX schnell (4 steps)."""
result = fal_client.subscribe(
"fal-ai/flux/schnell",
arguments={
"prompt": prompt,
"image_size": "landscape_16_9",
"num_inference_steps": 4,
"num_images": 1,
},
)
return result["images"][0]["url"]
Step 2: Image-to-Image and Inpainting
Beyond text-to-image, you can transform existing images:
def image_to_image(prompt: str, image_url: str, strength: float = 0.75) -> str:
"""Transform an existing image based on a text prompt."""
result = fal_client.subscribe(
"fal-ai/flux/dev/image-to-image",
arguments={
"prompt": prompt,
"image_url": image_url,
"strength": strength,
"num_inference_steps": 20,
"guidance_scale": 3.5,
},
)
return result["images"][0]["url"]
def inpaint(prompt: str, image_url: str, mask_url: str) -> str:
"""Inpaint a masked region of an image."""
result = fal_client.subscribe(
"fal-ai/flux/dev/inpainting",
arguments={
"prompt": prompt,
"image_url": image_url,
"mask_url": mask_url,
"num_inference_steps": 20,
"guidance_scale": 3.5,
},
)
return result["images"][0]["url"]
Inpainting is particularly useful for e-commerce (changing product backgrounds) and content creation (fixing specific areas of generated images).
Step 3: Batch Processing for Volume
Here is where the economics get interesting. Generating 100 product images at $0.01 each costs exactly $1.
import asyncio
from dataclasses import dataclass
@dataclass
class ImageJob:
prompt: str
filename: str
model: str = "fal-ai/flux/schnell" # Use schnell for batch (cheapest)
async def generate_batch(jobs: list[ImageJob], max_concurrent: int = 5) -> list[dict]:
"""Generate multiple images with concurrency control."""
semaphore = asyncio.Semaphore(max_concurrent)
results = []
async def process_job(job: ImageJob) -> dict:
async with semaphore:
result = await fal_client.subscribe_async(
job.model,
arguments={
"prompt": job.prompt,
"image_size": "square",
"num_inference_steps": 4,
"num_images": 1,
},
)
return {
"filename": job.filename,
"url": result["images"][0]["url"],
"prompt": job.prompt,
}
tasks = [process_job(job) for job in jobs]
results = await asyncio.gather(*tasks, return_exceptions=True)
successful = [r for r in results if not isinstance(r, Exception)]
failed = [r for r in results if isinstance(r, Exception)]
print(f"Generated {len(successful)} images, {len(failed)} failures")
return successful
# Example: Generate product images
async def generate_product_images():
products = ["wireless headphones", "smart watch", "laptop stand", "mechanical keyboard"]
backgrounds = ["white studio", "modern desk setup", "minimalist shelf"]
jobs = []
for product in products:
for bg in backgrounds:
prompt = f"Professional product photo of {product}, {bg} background, commercial photography, soft shadows"
filename = f"{product.replace(' ', '_')}_{bg.replace(' ', '_')}.png"
jobs.append(ImageJob(prompt=prompt, filename=filename))
results = await generate_batch(jobs)
# 12 images * $0.01 = $0.12 total
return results
# Run it
results = asyncio.run(generate_product_images())
At $0.01 per image with FLUX schnell, generating 100 product variations costs $1. With FLUX dev (higher quality, $0.03 each), 100 images cost $3. Compare this to stock photography at $5 to $50 per image.
Step 4: Build a FastAPI Service
Wrap the generation logic in an API service for your application:
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import hashlib
import json
import time
from typing import Optional
app = FastAPI(title="Image Generation API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["POST"],
allow_headers=["*"],
)
class GenerateRequest(BaseModel):
prompt: str = Field(..., min_length=3, max_length=1000)
model: str = Field(default="schnell", pattern="^(schnell|dev)$")
size: str = Field(default="landscape_16_9")
steps: Optional[int] = Field(default=None, ge=1, le=50)
class GenerateResponse(BaseModel):
image_url: str
model_used: str
cached: bool
cost_estimate: float
# Simple in-memory cache (use Redis in production)
image_cache: dict[str, str] = {}
def get_cache_key(request: GenerateRequest) -> str:
"""Generate a deterministic cache key for the request."""
data = json.dumps({
"prompt": request.prompt,
"model": request.model,
"size": request.size,
}, sort_keys=True)
return hashlib.sha256(data.encode()).hexdigest()[:16]
# Rate limiting (simple token bucket)
class RateLimiter:
def __init__(self, max_requests: int = 20, window_seconds: int = 60):
self.max_requests = max_requests
self.window = window_seconds
self.requests: list[float] = []
def is_allowed(self) -> bool:
now = time.time()
self.requests = [r for r in self.requests if now - r < self.window]
if len(self.requests) >= self.max_requests:
return False
self.requests.append(now)
return True
rate_limiter = RateLimiter(max_requests=20, window_seconds=60)
@app.post("/generate", response_model=GenerateResponse)
async def generate(request: GenerateRequest):
"""Generate an image from a text prompt."""
if not rate_limiter.is_allowed():
raise HTTPException(status_code=429, detail="Rate limit exceeded. Max 20 requests per minute.")
# Check cache
cache_key = get_cache_key(request)
if cache_key in image_cache:
return GenerateResponse(
image_url=image_cache[cache_key],
model_used=request.model,
cached=True,
cost_estimate=0.0,
)
# Determine model and steps
model_map = {
"schnell": "fal-ai/flux/schnell",
"dev": "fal-ai/flux/dev",
}
default_steps = {"schnell": 4, "dev": 20}
model_id = model_map[request.model]
steps = request.steps or default_steps[request.model]
cost = 0.01 if request.model == "schnell" else 0.03
try:
result = fal_client.subscribe(
model_id,
arguments={
"prompt": request.prompt,
"image_size": request.size,
"num_inference_steps": steps,
"num_images": 1,
"enable_safety_checker": True,
},
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
image_url = result["images"][0]["url"]
# Cache the result
image_cache[cache_key] = image_url
return GenerateResponse(
image_url=image_url,
model_used=request.model,
cached=False,
cost_estimate=cost,
)
@app.get("/health")
async def health():
return {"status": "ok", "cache_size": len(image_cache)}
Run with:
uvicorn main:app --host 0.0.0.0 --port 8000
Step 5: Cost Optimization Strategies
Here is how to keep your image generation costs under control at scale:
1. Cache aggressively
Same prompt with same parameters should never generate twice. In production, use Redis with a TTL:
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
def get_cached_or_generate(prompt: str, model: str) -> str:
cache_key = f"img:{hashlib.sha256(f'{prompt}:{model}'.encode()).hexdigest()[:16]}"
cached = r.get(cache_key)
if cached:
return cached.decode()
# Generate new image
url = generate_image(prompt)
# Cache for 7 days
r.setex(cache_key, 604800, url)
return url
2. Use the cheapest model that works
- Thumbnails and previews: FLUX schnell ($0.01) at 512x512
- Standard content: FLUX schnell ($0.01) at 1024x1024
- Hero images and marketing: FLUX dev ($0.03) at 1024x1024
- Maximum quality: FLUX dev ($0.04) at higher resolution
The difference between schnell and dev is subtle for most web content. Use dev only when the image will be displayed large or is customer-facing premium content.
3. Batch during off-peak hours
Queue non-urgent generation jobs for batch processing. Most APIs have slightly lower latency during off-peak hours (US nighttime), and batching reduces your code complexity.
4. Set spending alerts
class SpendingTracker:
def __init__(self, daily_limit: float = 5.0):
self.daily_limit = daily_limit
self.today_spent = 0.0
self.last_reset = time.time()
def can_spend(self, amount: float) -> bool:
# Reset daily counter
if time.time() - self.last_reset > 86400:
self.today_spent = 0.0
self.last_reset = time.time()
if self.today_spent + amount > self.daily_limit:
return False
self.today_spent += amount
return True
tracker = SpendingTracker(daily_limit=5.0) # Max $5/day
At $5/day, you can generate 500 schnell images or 166 dev images. That is plenty for most applications. For comparison, see my general guide on reducing LLM and AI API costs. The same tiered routing strategy from my AI API pricing comparison applies to image generation: use the cheapest model that meets quality requirements for each specific use case.
Step 6: Integration With Your Web App
Here is how to add image generation to an Astro or Next.js site:
API route (Next.js example):
// app/api/generate-image/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { prompt } = await request.json();
const response = await fetch("http://localhost:8000/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt,
model: "schnell",
size: "landscape_16_9",
}),
});
if (!response.ok) {
return NextResponse.json({ error: "Generation failed" }, { status: 500 });
}
const data = await response.json();
return NextResponse.json({ imageUrl: data.image_url });
}
Frontend component:
// components/ImageGenerator.tsx
"use client";
import { useState } from "react";
export function ImageGenerator() {
const [prompt, setPrompt] = useState("");
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleGenerate() {
setLoading(true);
try {
const res = await fetch("/api/generate-image", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
const data = await res.json();
setImageUrl(data.imageUrl);
} finally {
setLoading(false);
}
}
return (
<div>
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe the image you want..."
/>
<button onClick={handleGenerate} disabled={loading}>
{loading ? "Generating..." : "Generate"}
</button>
{imageUrl && <img src={imageUrl} alt={prompt} />}
</div>
);
}
Step 7: Production Deployment Checklist
Before deploying to production:
- Replace in-memory cache with Redis (or your preferred cache store)
- Add authentication to your generation endpoint
- Set up spending alerts at the provider level (fal.ai dashboard)
- Add request logging for debugging and cost tracking
- Store generated images in your own S3/R2 bucket (fal.ai URLs expire)
- Add content moderation on prompts before generation
- Set up fallback providers (if fal.ai is down, fall back to Replicate)
For routing between providers and managing fallbacks, OpenRouter handles LLMs but for image generation you need your own routing logic or a multi-provider client.
Project Structure
Here is the complete repository layout:
ai-image-generator/
βββ app/
β βββ __init__.py
β βββ main.py # FastAPI application
β βββ generator.py # Image generation logic
β βββ cache.py # Redis caching layer
β βββ rate_limiter.py # Rate limiting middleware
β βββ cost_tracker.py # Spending tracking and alerts
β βββ models.py # Pydantic request/response models
βββ tests/
β βββ test_generator.py
β βββ test_cache.py
β βββ test_api.py
βββ docker-compose.yml # Redis + app
βββ Dockerfile
βββ requirements.txt
βββ .env.example
βββ README.md
Cost Summary for Real Projects
| Use Case | Volume | Model | Monthly Cost |
|---|---|---|---|
| Blog images | 30/month | FLUX dev | $0.90 |
| E-commerce products | 500/month | FLUX schnell | $5.00 |
| Social media content | 200/month | FLUX schnell | $2.00 |
| Marketing campaigns | 100/month | FLUX dev | $3.00 |
| User-generated (SaaS) | 5,000/month | FLUX schnell | $50.00 |
For comparison, if you are generating more than 500 images/month and have the hardware, running FLUX locally eliminates the per-image cost entirely. The break-even is around 500 to 1,000 images/month depending on your hardware amortization.
If you want to run your own infrastructure instead of using APIs, check my guide on which models to run locally and the local-first developer stack for the complete setup. You can also use Ollama to serve local LLMs alongside your image generation pipeline for prompt enhancement.
FAQ
What is the cheapest API for AI image generation in 2026?
fal.ai offers FLUX schnell at $0.01 per image (1024x1024, 4 steps) and FLUX dev at $0.03 per image (20 steps). Replicate is slightly cheaper for some models but has cold start delays. For a full provider comparison with current pricing, see my image generation API pricing guide.
How many images can I generate per dollar?
With FLUX schnell on fal.ai: 100 images per dollar. With FLUX dev: approximately 33 images per dollar. With higher-resolution or multi-step generation: 15 to 25 images per dollar. Batch processing at lower resolution (512x512) can push this to 150+ images per dollar.
Is the fal.ai API reliable enough for production use?
In my experience over six months, fal.ai has had 99.7% uptime with occasional latency spikes during peak hours. For production, always implement a fallback provider (Replicate or Together AI) and cache aggressively. The images are served via CDN so retrieval is fast after generation.
Can I use generated images commercially?
FLUX schnell is Apache 2.0 licensed, so images generated with it have no model-imposed licensing restrictions. FLUX dev has a non-commercial license, but images generated through fal.aiβs API are covered under their commercial terms. Always check the specific providerβs terms of service for your use case.
How does API image generation compare to running FLUX locally for a small team?
For under 300 images/month, APIs are cheaper (no hardware cost). For 300 to 1,000 images/month, it depends on whether you already have a GPU. For over 1,000 images/month, local generation on an RTX 4090 ($44/month amortized) saves significantly. The API advantage is zero setup, instant scaling, and no maintenance. The local advantage is zero marginal cost, privacy, and no rate limits.