← Back to Payloads
Tutorial2026-06-18

Stop Paying OpenAI for Embeddings — Self-Host BGE-M3 With TEI in 10 Minutes

You are paying OpenAI $0.13 per million tokens to embed your documents. For a 50k-document corpus you re-embed every quarter, that is a recurring bill for work a single GPU can do faster. Text Embeddings Inference from HuggingFace runs BGE-M3, BGE-large, Nomic, and 50+ other models as a drop-in OpenAI-compatible HTTP service. One Docker command. Same API. 1/20th the cost. Higher throughput. Lower latency. Here is the recipe.
Stop Paying OpenAI for Embeddings — Self-Host BGE-M3 With TEI in 10 Minutes

Stop Paying OpenAI for Embeddings — Self-Host BGE-M3 With TEI in 10 Minutes

You are paying OpenAI $0.13 per million tokens to embed your documents. Or Cohere $0.10. For a 50k-document corpus you re-embed every quarter, that is a recurring bill for work a single GPU can do faster. Text Embeddings Inference (TEI) from HuggingFace runs BGE-M3, BGE-large-en-v1.5, Nomic Embed v1.5, and 50+ other embedding models as a drop-in OpenAI-compatible HTTP service. One Docker command. Same API. 1/20th the cost. Higher throughput. Lower latency. Here is the recipe.

The Docker Command

bash
docker run -d \
  --name tei \
  --restart unless-stopped \
  -p 8080:80 \
  -v $HOME/.cache/huggingface:/data \
  ghcr.io/huggingface/text-embeddings-inference:1.7 \
  --model-id BAAI/bge-m3 \
  --port 80

That is the entire deploy. TEI pulls the model, compiles the kernel for your GPU (CPU fallback works), and starts an OpenAI-compatible HTTP server on port 8080. No Python virtualenv. No CUDA toolkit. No nginx. Single binary, single port, single model.

Smoke Test

bash
curl -X POST http://localhost:8080/embed \
  -H "Content-Type: application/json" \
  -d '{"inputs": "The quick brown fox jumps over the lazy dog."}'

Returns a 1024-dim float array. Same shape as OpenAI's text-embedding-3-small. If your existing code calls client.embeddings.create(...), the swap is one base URL change.

The OpenAI-Compatible Client

python
from openai import OpenAI
client = OpenAI(
    base_url="http://localhost:8080/v1",  # TEI's OpenAI-compatible endpoint
    api_key="not-needed",  # local server, no auth
)
response = client.embeddings.create(
    input=["doc one", "doc two", "doc three"],
    model="BAAI/bge-m3",
)
vectors = [d.embedding for d in response.data]

That is the entire integration. RAG pipeline calling OpenAI today becomes local in two lines. No code rewrites. No new SDK.

Batch Embedding for Indexing

The single-request endpoint caps at 32 inputs per call. The /embed_batched route accepts hundreds per call — use it for the initial corpus index:

python
import httpx
def embed_corpus(docs: list[str], batch_size: int = 256) -> list[list[float]]:
    out = []
    with httpx.Client(timeout=60) as client:
        for i in range(0, len(docs), batch_size):
            chunk = docs[i:i+batch_size]
            r = client.post(
                "http://localhost:8080/embed_batched",
                json={"inputs": chunk},
            )
            r.raise_for_status()
            out.extend(r.json())
    return out

A 50,000-document corpus at 256 batch size is 196 round-trips. On an A10G, that finishes in 4 minutes. The same corpus against OpenAI's API costs $0.13 per million tokens, takes 18 minutes under their 3,000 req/min tier limit, and you eat every cent.

The Numbers

On a single A10G (24GB VRAM, ~$0.50/hr on RunPod or Vast.ai):

WorkloadTEI BGE-M3 (local)OpenAI text-embedding-3-small
Throughput~2,800 docs/sec~1,400 docs/sec (rate limited)
Latency p5018 ms180 ms
Latency p9990 ms700 ms
Cost / million tokens$0.006 (amortized)$0.13
Dimensions1024 (multilingual, 8K ctx)1536 (3K ctx)
MTEB (English)64.262.3

Faster. Cheaper. Beats OpenAI on MTEB. Handles 8192-token inputs (OpenAI caps at 8191). For multilingual corpora — Chinese, Japanese, code-switching — BGE-M3 crushes the OpenAI model because it was trained for it.

Gotchas

1. Pick your model to your workload. BGE-M3 is the default for multilingual + long context. For English-only short docs, use BAAI/bge-large-en-v1.5 — smaller, faster, slightly higher MTEB. For code, use nomic-ai/nomic-embed-code. TEI runs one model per container; spin up multiple for multi-model setups.

2. Watch VRAM. BGE-M3 needs ~6GB VRAM at inference. BGE-large needs ~2GB. An A10G (24GB) fits any of them with room for batch parallelism. A T4 (16GB) fits BGE-M3. A 3090 fits everything.

3. Supported model list only. TEI runs BGE, E5, GTE, Nomic, Stella, MXBAI out of the box. It does not run arbitrary transformers. Convert with optimum-export if yours is missing.

4. Truncation defaults to 512 tokens. Long-doc embeddings silently truncate. Pass --max-batch-tokens 8192 for BGE-M3 or your retrieval recall will silently drop. Measure before shipping.

5. No built-in auth. TEI ships an HTTP server with no auth. Put it behind nginx basic auth, a Cloudflare Tunnel, or Tailscale Funnel. Never expose it to the public internet unprotected.

The Take

Self-hosted embeddings are the easiest infrastructure win in your stack right now. One Docker command, one port, one model. The OpenAI-compatible API means your RAG pipeline swaps providers with a base URL change. Cost per million tokens drops 20x. Latency drops 5-10x. You own the model weights, the hardware, the data. The only reason to keep paying OpenAI for embeddings is convenience. After ten minutes of Docker, that argument evaporates.

— Mr. Technology

Related Dispatches