Runbooks/RAG RunbookTrack B · Embeddings and indexLLM Inference Runbook →0%
  1. 00 Start
  2. /
  3. 01 Chunking
  4. 02 Parsing
  5. 03 Identity
  6. 04 Access
  7. /
  8. 05 Models
  9. 06 Vectors
  10. 07 Limits
  11. 08 Model ops
  12. 09 Index I
  13. 10 Index II
  14. 11 Tuning
  15. 12 Capacity
  16. /
  17. 13 Sharding
  18. 14 Filtering
  19. 15 Hybrid
  20. /
  21. 16 Proof
RAG Runbook · Document 07 of 16 · Track B — Embeddings and index

Track B · Document 07 · Embeddings and index

Token Limits, Truncation and the Embedding Pipeline

A ceiling you cannot raise, a failure that raises no error, and the reason a model with room to spare is worth paying for even though your chunks are small.

Reads in about 30 minutes · 8 figures, 2 of them interactive · 8 interview questions · prints to clean A4

What is in this document

  1. Two numbers that get confused
  2. What the limit actually is
  3. Real limits on real models
  4. What truncation actually does
  5. Tokens are not characters
  6. The opposite mistake
  7. What the headroom is for
  8. Chunk size and model limit together
  9. Long documents
  10. The query side
  11. Detection and proof
  12. The embedding client contract
  13. Symptom → cause
  14. Interview questions
  15. FAQ
  16. Cheat sheet

1 · Two numbers that get confused

Every embedding model refuses to read past a certain amount of text. That ceiling is measured in tokens and it is fixed at training time: you cannot raise it, configure it, or pay for more of it.

Before anything else, separate it from the number it gets confused with.

TWO UNRELATED NUMBERS THAT GET MIXED UP CONSTANTLY INPUT TOKEN LIMIT how much text goes in 512 or 8192 the encoder a few hundred million parameters OUTPUT DIMENSION how many numbers come out 384, 768, 1024, 1536, 3072 a model with a 1024-dimension output and a 512-token input limit is entirely normal set by position handling in the encoder the size of the final hidden layer can you change it no — fixed at training time sometimes — Matryoshka truncation what breaks it text that is too long nothing; it is always the same The output size never changes. That is precisely why truncation is invisible. The shape of the result is identical whether the model read all of your text or half of it. Every downstream system accepts it, because there is nothing to reject.

Exceeding the limit is not an error. The model does not complain, does not warn, and does not return anything unusual. It reads what it can, discards the rest, and hands back a perfectly well-formed vector. The damage appears months later, as questions that never find their answers.

What this document decides

And it matters more than it looks for one reason: this is the same failure family as the prefix bug and the normalisation asymmetry. Silent, structural, and invisible in every log you have.

2 · What the limit actually is

The limit is not a business rule or a safety valve. It is a structural property of the encoder, and understanding where it comes from is what lets you answer the follow-up question about why some models have 512 and others have 8192.

A LEARNED LOOKUP TABLE — ONE ROW PER POSITION Attention has no built-in notion of order. Left to itself it treats the input as a bag of tokens, and dog bites man becomes indistinguishable from man bites dog. So before the first layer, the model adds a position signal to every token: “I am first”, “I am second”, and so on. row 1 [0.02, -0.11, …] row 2 [0.07, 0.31, …] row 512 [-0.14, 0.08, …] row 513 does not exist Every row was trained by seeing real text at that position, and the table’s height was fixed during pre-training. There is no row 513. Not an untrained row — no row. So token 513 has no position signal to add, and the forward pass simply cannot accept it. That table’s height is your token limit. “Max sequence length 512” is the number of rows. The limit is not a business rule or a safety valve, and no amount of paying for a larger tier changes it. It is a structural property of the encoder, and the only way to raise it is to train a different model. A FORMULA — ROTARY AND RELATIVE POSITION SCHEMES The fixed table has an obvious weakness: supporting longer inputs means training a taller table, which means retraining. Newer models avoid the table entirely and compute the position signal instead. signal(9) = f(9) signal(9000) = f(9000) position 9000 is exactly as computable as position 9 There is no ceiling in the arithmetic. So why does the model card still say 8192? Because that is the length it was trained and validated at — not because anything breaks above it. Quality degrades gradually past the trained range rather than failing outright — which is arguably worse, because it is even quieter. Other schemes exist. The architectural detail matters less than the operational point: the number on the model card is the number you must respect, regardless of how it is produced internally.

Both tabs end in the same place. One has a hard wall you cannot cross and the other has a soft slope you should not walk down — and in an interview, knowing that the second one exists and is quieter is the part that lands.

3 · Real limits on real models

Grounded numbers you can quote — with the usual caveat that vendors ship new versions, so re-check the model card before any interview.

Model familyInput limitOutput dimension Position scheme
BERT-based encoders, generally512768 learned table
all-MiniLM-L6-v2 256 configured (512 architectural)384 learned table
E5 and multilingual-E5512384–1024 learned table
Cohere embed v35121024 learned table
OpenAI text-embedding-381911536 / 3072 modern
BGE-M381921024modern
Nomic and Jina long-context embedders8192768 rotary / ALiBi

Two things to notice in that table

Embedding models are small. A few hundred million parameters, not billions. They have to be — you run one over every chunk in the corpus, and then over every query, forever. A seven-billion-parameter embedder would be economically absurd at ten million chunks, and saying so is a good way to show you have thought about where the cost actually lands.

The MiniLM row is the trap. The underlying architecture supports 512, but the shipped configuration truncates at 256. Teams read the architecture number, size their chunks to 512, and lose half of every chunk. The configured value is what runs, not the architectural one — and the only way to know is to look at the config rather than the paper.

4 · What truncation actually does

A concrete case, traced all the way to a user. The chunk is 700 tokens and the limit is 512.

A 700-TOKEN CHUNK MEETS A 512-TOKEN LIMIT the chunk — 700 tokens, stored in full in the payload 700 what the model reads — 512 tokens discarded — 188 cut The tokeniser slices at 512 and the model returns a perfectly normal-looking vector, built from the head only. No warning, no error, right dimension, right norm. a query about the head matches the vector → chunk retrieved → the model receives the whole chunk, tail included, and answers correctly a query about the tail the vector knows nothing of it → scores low → never enters the top k → the answer never reaches the model Same chunk. Inconsistent behaviour. No log line anywhere. The content is stored, indexed, and unfindable — which is a genuinely strange state for a system to be in, and nothing in it is broken.
  1. The chunk. 700 tokens, stored in full in the payload.
  2. The cut. The tokeniser slices at 512; the model never sees the last 188 tokens and returns a normal-looking vector built from the head only.
  3. A query about the head. Matches the vector, the chunk is retrieved, and the model receives the whole chunk — tail included — so it answers correctly.
  4. A query about the tail. The vector knows nothing about it, so the chunk scores low and is missed. Same chunk, inconsistent behaviour, no log line anywhere.

Note what step 3 does to your ability to detect this. Half the questions about that chunk work perfectly, which is exactly what makes spot-checking useless as a defence.

Someone asks a question whose answer sits in those last 188 tokens. That chunk is the right chunk: it contains the answer, in plain text, in your database. But the vector was built without that text, so it describes only the first part. The query scores poorly against it, it does not appear in the top k, and the answer never reaches the model.

5 · Tokens are not characters

This is where the limit bites in practice, because you size chunks in one unit and the model counts in another. English prose runs at roughly four characters per token — a heuristic that is safe enough for prose and badly wrong for anything structured.

SAME CHARACTER COUNT, VERY DIFFERENT TOKEN COUNT prose "The payment service returns a timeout after thirty seconds." 60 chars → ~11 tokens identifier "ERR_5521_PAYMENT_GATEWAY_TIMEOUT_UPSTREAM_X9" 44 chars → ~15 tokens uuid "f47ac10b-58cc-4372-a567-0e02b2c3d479" 36 chars → ~20 tokens Tokenisers are trained on natural language, so common English words are single tokens. Random hex, underscored identifiers and JSON punctuation shatter. your chunk, estimated limit 512 ~500 tokens under a 512 limit, but with almost no margin The same character count as a config file would be ~1,000 tokens, and would truncate. The rule: never estimate. Count with the model’s own tokeniser — not a generic one, not divide-by-four. This dial is for intuition, not for production.

A chunk that is 400 tokens of documentation prose can be 900 tokens of a config file at the same character count. If your chunker is character-based and your corpus is mixed, the technical documents truncate while the prose does not — which produces the very common bug where recall is fine overall and terrible for one document type.

Content typeRough characters per token Risk at a 512-token limit
English prose~4.0low
Technical documentation~3.5moderate
Code~2.5high
JSON or YAML configuration~2.0high
Logs with IDs and stack traces~2.0 or worse very high
Tables rendered as text~2.5high

The rule: never estimate. Count with the model’s own tokeniser — not a generic one, not a divide-by-four. Different models tokenise the same string into different counts, so the tokeniser is part of the model dependency, not a utility.

6 · The opposite mistake

Everything above argues for staying under the limit. Here is the counter-intuitive half, and the part interviewers use to separate levels.

A LONG LIMIT IS NOT PERMISSION TO USE LONG CHUNKS TRUNCATION cause · the chunk exceeds the limit the model · drops the tail symptom · some content is unfindable error raised · none DILUTION cause · the chunk is long but perfectly legal the model · reads everything symptom · everything matches weakly error raised · none Both are silent. Both are fixed upstream. Neither shows up in a log. And the fix for both is the same: smaller chunks. Why vagueness happens The output vector has fixed capacity — 1024 numbers, whatever the input length. Feed in one focused idea and the vector points firmly at it. Feed in fifteen unrelated ideas and it lands somewhere in the middle of all of them. Useful chunks land at 200 to 500 tokens regardless of what the model permits.

The model’s limit is a ceiling, not a target. This is the half of the topic interviewers use to separate levels: everyone knows text can be too long for the model, and rather fewer volunteer that text can be well within the limit and still too long to be useful.

7 · So what is the headroom actually for?

If chunks should be 200 to 500 tokens regardless, why pay for a model with an 8192 limit at all? Because the headroom buys context, not length.

RAW CHUNK
  "The timeout defaults to thirty seconds and can be raised to a maximum
   of five minutes. Values above this are rejected."

   Which service? Which API? Which version? The chunk never says.

CONTEXTUALISED CHUNK
  "From: Payments API Guide v4 > Configuration > Request handling.

   The timeout defaults to thirty seconds and can be raised to a maximum
   of five minutes. Values above this are rejected."
THE HEADROOM BUYS CONTEXT, NOT LENGTH what actually gets embedded title and section path generated summary of the parent metadata the actual chunk content against a 512-token limit 555 — over by 43 you would have to cut real content to make room for the context against an 8192-token limit 555 — 7,637 to spare trivially affordable, and that is the real argument for a long-context embedder Not bigger chunks. Affordable context on normal-sized chunks. A raw chunk often cannot stand alone: “the timeout defaults to thirty seconds” never says which service, because that was in the document title three pages up. Prepend the path and a short summary and the vector encodes both what it says and where it sits — which matters most on large corpora.

This reframes the model-limit decision entirely. The question is not “how long are my chunks?” — they should be 200 to 500 tokens either way. It is “how much situating context can I afford to prepend to each one?”

8 · Choosing chunk size and model limit together

The question interviewers ask is which one decides the other. Getting the direction right is the whole answer.

THE WRONG DIRECTION “The model does 512 tokens, so we chunk at 512 tokens.” — a technical constraint has just chosen your retrieval strategy. THE RIGHT DIRECTION 1 · the right chunk size from retrieval quality, measured on a gold set 2 · add the context path, summary, metadata — 80 to 230 tokens 3 · count it, properly real tokeniser, special tokens, p99 not median 4 · choose the limit the model fits the design, not the other way round Step 4 converts a silent truncation bug into an explicit, costed architectural choice. That is the whole point of the ordering. And why shrinking the chunks is usually the worst way out Below a certain size you stop cutting on semantic boundaries and start cutting through the middle of ideas. A procedure with eight steps becomes two chunks of four, and now no single chunk answers “how do I do X”. You have traded a truncation problem for a completeness problem — a different bug, not a fix.

The question interviewers ask is which one decides the other, and getting the direction right is the whole answer. The chunk size that is best for retrieval has nothing to do with the number of rows in somebody’s position embedding table.

9 · Long documents: do not embed the document

A fifty-page document does not fit anywhere useful, and even inside a 32,000-token model it would produce a hopelessly diluted vector. The architectural answer is the one you already have from document 01: parent–child retrieval.

search onsmall children, 200–500 tokens, well inside any model’s limit
returnthe parent section, which is never embedded and therefore never constrained
so the limit constrainsonly the child size, which was going to be small anyway

Search small for precision, return large for completeness. That is why “how do I embed a fifty-page document?” is a slightly wrong question, and saying so politely — then giving the right question — is a good interview move. It is the same move as “which region grew fastest?” being a text-to-SQL question rather than a chunking one.

10 · The query side

Queries are short. Fifty tokens is a long question, so the limit is usually a non-issue on the read path — until one of four things happens.

CaseWhy it growsTypical size
Conversation history prependedThe whole chat becomes the query can exceed 512
Query expansion or HyDE A generated hypothetical answer is embedded instead of the question 200–600
Multi-query rewriting, concatenatedSeveral rewrites joined into one string 300–800
Similarity search by exampleA whole document is used as the query unbounded

In all four, exceeding the limit truncates silently, exactly as it does on the ingest side. The user sees a slightly worse answer and nothing else.

Where the check belongs

In the same shared embedding function that both paths call. Document 05 established that for the prefix convention and document 06 for normalisation; the token assertion belongs in exactly the same place, for exactly the same reason. One function, two callers, one guarantee.

If ingest and query use two different code paths, you will eventually have two different limits, and the difference will be silent.

11 · Detection and proof

You cannot detect truncation from a stored vector. It looks completely normal — right dimension, right norm, right distribution. There is no forensic signature. So detection has to happen before embedding, at ingest time, and it takes four layers.

YOU CANNOT DETECT TRUNCATION FROM A STORED VECTOR. THERE IS NO FORENSIC SIGNATURE. Right dimension, right norm, right distribution. Detection therefore has to happen before embedding, at ingest time, in four layers. 1 · the assertion count with the real tokeniser, include special tokens, raise — never truncate 2 · the p99 metric publish the token-count distribution, not the mean it shifts before it breaks 3 · the CI test fixed texts, expected counts including a token-dense one catches a tokeniser change 4 · the corpus audit walk what is already stored and mark it for re-chunking the only backward-looking one enc = tokenizer(text, add_special_tokens=True) if len(enc["input_ids"]) > MODEL_TOKEN_LIMIT: raise ValueError(f"chunk {chunk_id} is {n} tokens, limit {MODEL_TOKEN_LIMIT}") Most libraries truncate by default. That is the setting that causes this bug. In the common sentence-transformers and pipeline wrappers, truncation is on unless you turn it off. The library is doing what it was asked to do; the problem is that nobody asked.

Four layers, and only the fourth looks backwards. The first three are cheap and permanent; the audit is the one that tells you how much of the corpus was quietly damaged before anyone was watching.

The audit, which is the one nobody runs

for each stored chunk:
    n = count_tokens(chunk.text)          # the model's own tokeniser
    if n > LIMIT:
        mark for re-chunk and re-embed

report: how many, which document types, which ingest dates

The last line is the valuable one. The shape of the answer usually names the cause: if the affected chunks cluster in one document type, your chunker is character-based and that type is token-dense. If they cluster in one date range, a pipeline change caused it and you can find the deploy.

12 · The embedding client contract

Three documents have now arrived at the same conclusion from three different directions, so it is worth stating the conclusion once, properly. There should be exactly one function in your system that turns text into a vector, and it should own every convention.

# embedding_client.py — the only place text becomes a vector

MODEL   = "text-embedding-3-large@2024-01"   # pinned, not "latest"
LIMIT   = 8191                                # from the config, not the paper
TOK     = load_tokenizer(MODEL)               # the model's own, versioned with it

def _embed(text: str, role: str) -> list[float]:
    prefixed = ROLE_PREFIX[role] + text              # document 05
    n = len(TOK(prefixed, add_special_tokens=True))  # document 07
    if n > LIMIT:
        raise TooLong(chunk_id, n, LIMIT)            # raise, never truncate
    v = model.encode(prefixed)
    v = normalise(v)                                 # document 06
    assert abs(norm(v) - 1.0) < 1e-6
    return v

def embed_document(text): return _embed(text, "document")
def embed_query(text):    return _embed(text, "query")

# There is deliberately no function that takes raw text
# without declaring what it is for.
What the client ownsThe failure it preventsCovered in
The role prefixQuery and documents land in different regions of the space; recall falls by a thirdDocument 05
The model and version pinA vendor version bump silently changes the space under a live indexDocument 05
Normalisation, and the assertion after it Asymmetric norms, broken thresholds, uneven quantisation damage Document 06
Truncation and renormalisation, together Documents whose magnitude sits early get an unearned ranking bonus Document 06
The token count, with special tokens, raising rather than truncating Content that is stored, indexed and unfindableThis document
Batching by token budget rather than by item count A batch of token-dense chunks blowing a per-request limit that a batch of prose would notThis document

Batch by tokens, not by items

A detail worth volunteering, because it follows directly from section 5. “Send 64 chunks per request” is a rule that works until 64 config files arrive together and the batch is three times the size the same count of prose would be. Accumulate up to a token budget, with an item cap as a secondary guard and a short flush timeout so a trickle of edits does not wait indefinitely behind a half-full batch.

The operational side of running this at scale — rate limits, backpressure, retries, dead-lettering, and the calls-per-changed-document metric — is in document 03. What belongs here is the interface: one function, every convention, no way around it.

13 · Symptom → cause

SymptomMost likely causeWhat to check first
Recall is fine overall and terrible for one document type Character-based chunking on a token-dense format: config, code, logs Token counts by document type. This is the signature of the bug
Some questions about a chunk work and others do not That chunk was truncated; the head is searchable and the tail is not Token count of the stored text against the model’s configured limit
Everything matches everything, weakly Dilution — the chunks are long but legal The token-count distribution. If the median is over about 600 you are diluting
Recall dropped after upgrading a library, with no config change The tokeniser changed, or a default truncation setting changed The CI test that asserts token counts on fixed texts. If you do not have one, this is why you need one
Chunks sized exactly at the limit still truncate Special tokens. The encoder adds markers that consume positions Whether the count includes add_special_tokens=True
Long conversational queries return worse results than short ones The query side is exceeding the limit once history is prepended Whether the token assertion runs on the query path as well as the ingest path
A model with a 512 limit is losing half of every chunk The configured limit is lower than the architectural one The shipped configuration, not the model card or the paper
Recall improved after you shortened chunks and then got worse again You went below the natural semantic unit and traded truncation for incompleteness Whether single chunks still answer whole questions, or whether two are now needed

14 · Interview questions

ArchitectWhat happens if a chunk is longer than the model’s token limit?

Nothing visible, which is the problem. The tokeniser cuts at the limit, the model embeds the head and returns a perfectly normal vector — right dimension, right norm — and every downstream system accepts it because there is nothing to reject.

The damage shows up much later and asymmetrically: questions about the head of that chunk work fine, and questions about the tail never find it. The content is stored, indexed and unfindable. So the defence has to be an assertion before embedding, because there is no forensic signature in the stored vector afterwards.

ArchitectWhy do some models have a 512-token limit and others 8192?

It is how they handle position. Classic encoders add a learned position signal from a lookup table with one row per position, and the table’s height was fixed during pre-training — so there is no row 513, and the forward pass cannot accept token 513.

Newer models compute the position signal from a formula instead, so position 9000 is as computable as position 9. Those advertise 8192 or 32,000 because that is what they were trained and validated at, not because anything breaks above it — and past that range they degrade gradually rather than failing, which is quieter and arguably worse.

ArchitectOur model accepts 8192 tokens. Should we use 8000-token chunks?

No, and this is the more interesting half of the topic. Nothing truncates, so there is no error — the vector is simply vague. The output has fixed capacity whatever the input length, so fifteen ideas compressed into 1024 numbers lands somewhere in the middle of all of them and matches everything weakly.

Useful chunks land at 200 to 500 tokens regardless of what the model permits. The limit is a ceiling, not a target. What the extra headroom actually buys is context — a section path and a short summary prepended to a normal-sized chunk, which is worth 80 to 230 tokens and improves recall materially on large corpora.

ArchitectHow do you size chunks against a model limit?

In that order — chunk size first, model second. Start from what the natural semantic unit of the corpus is and what a sweep against the gold set says. Add the context you intend to prepend. Then count it with the model’s own tokeniser, including special tokens, on the densest content type rather than the average one. Then choose a model whose limit accommodates that.

The wrong direction is “the model does 512, so we chunk at 512”, which lets the number of rows in somebody’s position table choose your retrieval strategy. And if no acceptable model has the headroom, that is now an explicit costed choice rather than a silent truncation nobody decided on.

ArchitectHow would you detect that truncation has been happening?

Four layers, and only the last one looks backwards. An assertion at ingest that counts with the real tokeniser, includes special tokens and raises rather than truncating — because most libraries default to silent truncation. A p99 token-count metric on a dashboard, so a new document type shifts the distribution before it breaks anything. A CI test over fixed texts, including a deliberately token-dense one, which catches a tokeniser change or a config change.

And then an audit over what is already stored, reporting how many chunks are over the limit, by document type and ingest date — because the shape of that report usually names the cause.

ArchitectHow do you embed a fifty-page document?

You do not, and I would say so politely and then give the better question. Even in a 32,000-token model, one vector for fifty pages is hopelessly diluted — it would be close to everything and specific to nothing.

The architectural answer is parent–child: embed small children, store the parent as text, search small for precision and return large for completeness. The model’s limit then constrains only the child size, which was going to be 200 to 500 tokens anyway.

Eng managerA team reports that retrieval is bad for one product’s documentation only. Where do you point them?

Token counts by document type, before anything else. That symptom — fine overall, bad for one slice — is the signature of character-based chunking meeting a token-dense format. If that product’s docs are full of configuration examples, code blocks or log excerpts, they are running at roughly two characters per token while the prose corpus runs at four, so the same chunker produces chunks twice as long in token terms and they truncate.

The reason I go there first is that it costs ten minutes to check and it explains the shape of the complaint, which most retrieval hypotheses do not.

Eng managerWhat would you put in the definition of done for the embedding path?

One shared client that owns every convention — the role prefix, the pinned model version, normalisation, truncation with renormalisation, and the token assertion — with deliberately no function that takes raw text without declaring its role. Then three tests in CI: gold-set recall above a threshold, norms equal to one on both paths, and token counts on fixed representative texts.

The framing I would use with the team is that retrieval fails quietly and almost everything else fails loudly. Other subsystems can rely on exceptions to tell them something is wrong; this one cannot, so it needs assertions instead. That explains why the tests are not optional, which is usually the actual argument.

15 · FAQ

Can I just let the library truncate and accept the loss?

You can, and the reason not to is that you will not know how much you lost or where. Truncation is not uniform — it hits your token-dense documents and leaves the prose alone, so it damages one part of the corpus and looks like a general quality problem. If you genuinely decide to accept truncation, at least count it: log how many chunks were cut and by how much, so the decision stays visible.

Do special tokens really matter for a 512-token limit?

Yes, and this is a real edge case rather than a pedantic one. Encoders add markers at the start and end of the sequence, and those consume positions. A chunk that is exactly 512 tokens before special tokens is over the limit after them, so a chunker that targets the limit exactly will truncate every single chunk by a token or two. Always count with add_special_tokens=True, and target slightly below the limit.

Is a longer limit always better if I can afford it?

It is never worse, and it is often not the differentiator people assume. The headroom is genuinely valuable — it is what makes contextual prefixes affordable — but a long-context model that is weaker at retrieval is a bad trade, because you would be paying in the metric that matters for room you may not need. Measure both on the gold set; treat the limit as one filter among several rather than as the headline feature.

Should the chunker count tokens or characters?

Tokens, using the model’s own tokeniser, if you can afford it — and you usually can, because tokenising is cheap compared with embedding. Character-based chunking is the root cause of the one-document-type failure, and switching to token-based removes an entire class of bug. Where a character-based splitter is unavoidable, at least set the target from the densest content type in the corpus rather than the average.

What is HyDE, and why does it show up in a document about token limits?

Hypothetical document embedding: instead of embedding the user’s question, you have a language model write a plausible answer and embed that, on the theory that an answer looks more like the passages you are searching than a question does. It appears here because it turns a fifteen-token query into a two-hundred-to-six-hundred-token one, which puts the query path near a limit nobody was watching — and truncating half of a generated hypothetical answer is a strange and very quiet way to lose recall.

Our chunks are 300 tokens and we still have recall problems. Is the limit the issue?

Almost certainly not, and that is worth establishing quickly so you stop looking here. At 300 tokens against any modern limit you are neither truncating nor diluting. The usual suspects then are the prefix convention, normalisation asymmetry, a chunk that has lost the context it needed, or identifier-shaped queries that dense retrieval simply cannot serve — documents 05, 06, 01 and 15 respectively.

Does the token limit apply to the metadata I store, or only to the text I embed?

Only to what you send to the model. Metadata you store for filtering and citation costs storage, not tokens. The confusion is worth resolving carefully though, because the section path is often both: stored as a field, and prepended to the text before embedding. When it is prepended, it counts.

How much context should I prepend?

Start with the section path alone, because it is free and deterministic, and measure. Add a generated summary only if the gold set shows it helps — it is 50 to 150 tokens on every chunk plus a generation call at ingest, and on some corpora it buys almost nothing because the section path already carried the missing words. The budget is real: at 400 tokens of content and 230 of context you are at 630, which fits comfortably in 8192 and not at all in 512.

16 · Cheat sheet

The numbers

classic encoders 512 tokens — a learned position table with 512 rows
modern / rotary 8192 or more — computed, so the limit is the trained range
MiniLM 256 configured, 512 architectural — the configured value wins
useful chunk size 200–500 tokens, whatever the model permits
context prefix budget 80–230 tokens: path 20–50, summary 50–150, metadata 10–30
chars per token prose ~4.0 · docs ~3.5 · code ~2.5 · JSON ~2.0 · logs ~2.0 or worse
a long query ~50 tokens — until history, HyDE or multi-query make it 800

The one-liners

The ninety-second version

“Every embedding model has a hard input limit fixed at training time, and exceeding it is not an error — the tokeniser cuts, the model embeds the head, and you get a perfectly normal vector back. So the chunk is stored, indexed and unfindable for any question about its tail, and there is no signature in the stored vector to find afterwards. Detection has to be an assertion before embedding.

The counter-intuitive half is that a long limit is not permission to use long chunks. The output vector has fixed capacity, so a legal 6,000-token chunk is not truncated, it is just vague. Useful chunks are 200 to 500 tokens either way, and what the headroom actually buys is context — a section path and a short summary prepended, 80 to 230 tokens, which is what makes a chunk findable when the words in the question never appear in it.

And I would size in that order: chunk size from retrieval quality, plus the context I want, counted with the real tokeniser on the densest content type, and then pick a model whose limit fits. The failure I would specifically guard against is character-based chunking on a mixed corpus, because config files run at half the characters per token that prose does, so one document type truncates while everything else looks fine.”

Where this connects

Thread from this documentResolved in
Why chunks want to be 200–500 tokens in the first place 01 · Chunking foundations
Token-dense content: code, config, logs, tables 02 · Parsing hard content
Rate limits, retries and the ingest fleet 03 · Identity, updates and deletes
The prefix convention the same client owns 05 · Choosing an embedding model
Normalisation and truncation, in the same function 06 · Dimensions, metrics and Matryoshka
HyDE and multi-query, which grow the query 15 · Hybrid retrieval and reranking
The CI gate that catches all of this 16 · Evaluation and observability

Questions to ask them