Key Takeaway
Indonesian text costs about 23 percent more tokens than the same meaning in English on OpenAI's o200k_base encoding, measured across 20 matched sentence pairs, not the two to three times widely repeated. The older cl100k_base charged 52 percent. Three of those 20 Indonesian sentences were actually cheaper than their English twin.
I was costing an Indonesian-language support assistant and needed exactly one number: how much more does Bahasa Indonesia cost per message than English. Every source I found repeated the same 2x to 3x multiplier, none of them showed a measurement, and most of them were quietly citing each other. At 2.5x the project was marginal. At 1.2x it was fine. Nobody could tell me which one it was.
So on 18 September 2026 I measured it. Twenty English and Indonesian sentence pairs, written by hand in the register a real product actually sends - support replies, ERP notices, product copy, privacy text - run through the gpt-tokenizer package against both o200k_base and cl100k_base. This post is that measurement: what the number is, why it moved, why averaging it is misleading, and which mitigations are worth the trouble.
Twenty matched sentence pairs, two encodings, one honest number
Indonesian cost 23 percent more tokens than English for the same 20 sentences on o200k_base, the encoding behind the GPT-4o and GPT-5 families. The corpus came to 300 English tokens against 369 Indonesian ones. On cl100k_base, the older encoding used by GPT-4 and GPT-3.5 Turbo, the same text ran 300 against 456 - a 52 percent penalty. The English side did not move by a single token between the two encodings.
| Encoding | Model family | EN chars per token | ID chars per token | Corpus tokens EN / ID | ID to EN ratio |
|---|---|---|---|---|---|
| o200k_base | GPT-4o and GPT-5 families | 5.40 | 4.38 | 300 / 369 | 1.23x |
| cl100k_base | GPT-4 and GPT-3.5 Turbo | 5.40 | 3.54 | 300 / 456 | 1.52x |
Characters per token is the unit that survives this comparison, because a raw token count rewards whichever language says the thing in fewer characters. In this corpus neither does: the English half is 1,619 characters and the Indonesian half is 1,615, four characters apart. The two halves are the same length on the page and a different length in the billing system, which is the whole point. English packs 5.40 characters into a token here and Indonesian 4.38. The measurement needs no API key and finishes in under a second.
// npm i gpt-tokenizer@4.0.0 - OpenAI's BPE encodings, offline, no API key.
import { encode as o200k } from "gpt-tokenizer/encoding/o200k_base";
import { encode as cl100k } from "gpt-tokenizer/encoding/cl100k_base";
// Both sides hand-written by the same person in the same register. Feeding
// machine translation in here measures the translator, not the language.
const pairs = [
["The refund has been processed and will appear in your account within three working days.",
"Pengembalian dana sudah diproses dan akan masuk ke rekening Anda dalam tiga hari kerja."],
["Your subscription will renew automatically unless you cancel it before the billing date.",
"Langganan Anda akan diperpanjang secara otomatis kecuali Anda membatalkannya sebelum tanggal penagihan."],
// ...18 more: support replies, ERP notices, product copy, privacy text
];
for (const [name, enc] of [["o200k_base", o200k], ["cl100k_base", cl100k]]) {
let en = 0, id = 0, enChars = 0, idChars = 0;
for (const [a, b] of pairs) {
en += enc(a).length; id += enc(b).length;
enChars += a.length; idChars += b.length;
}
// chars/token, not raw token count. Raw counts flatter whichever language
// happens to need fewer characters to say the same thing.
console.log(name, (id / en).toFixed(2) + "x",
"EN", (enChars / en).toFixed(2), "ID", (idChars / id).toFixed(2));
}
// o200k_base 1.23x EN 5.40 ID 4.38
// cl100k_base 1.52x EN 5.40 ID 3.54gpt-tokenizer is a pure TypeScript implementation of OpenAI's BPE encodings with no native dependency and no network call, so you can count a million strings in CI without a key or a bill. Version 4.0.0 exposes each encoding behind its own import path, so you load the one vocabulary you need rather than all of them.
Where the 23 percent comes from: one Indonesian word, five tokens
The penalty is affixation. Indonesian builds a single written word out of a prefix, a root, a suffix and often a clitic, and these encodings learned their vocabulary on text that is overwhelmingly English, so a long Indonesian word has no token of its own and gets cut into pieces. Decoding each token id back to its own string shows exactly where the cuts land.
// Each token id decoded back to its own string, so you can see the seams.
// o200k_base, the encoding behind the GPT-4o and GPT-5 families.
membatalkannya 5 tokens m | emb | at | alk | annya
ketidaknyamanannya 5 tokens ket | idak | ny | aman | annya
dijadwalkan 5 tokens d | ij | ad | walk | an
memverifikasi 3 tokens mem | ver | ifikasi
penghapusan 3 tokens peng | hap | usan
// One Indonesian orthographic word is often prefix + root + suffix + clitic.
// "membatalkannya" is meN- plus batal plus -kan plus -nya: cancel it. English
// spends three cheap words on that; Indonesian spends one expensive one.
// The other direction is why the tax is 1.23x and not 2x. The loanwords a
// real product leans on are already whole tokens:
invoice 1 token invoice
pelanggan 2 tokens pel | angganThe second half of that output is why the tax is 1.23x and not 2x. Formal Indonesian is affix-heavy, but the working vocabulary of a real Indonesian product is full of English loanwords that already have a token to themselves - invoice was one token in both encodings in my corpus - and Indonesian spends nothing on the articles English pays for on every noun. The affixed verbs cost; a lot of the nouns do not. Which of the two your copy is made of decides the ratio you end up with.
The tax halved between cl100k_base and o200k_base
1.52x became 1.23x with no change to the text at all. o200k_base carries a substantially larger vocabulary than cl100k_base, and the extra slots went disproportionately to sequences that are not English, because English was already well covered. Three words from the corpus show the mechanism directly:
- pengiriman - 3 tokens on cl100k_base, 2 on o200k_base
- keuangan - 3 tokens on cl100k_base, 2 on o200k_base
- persetujuan - 4 tokens on cl100k_base, 3 on o200k_base
The English half of the corpus came to exactly 300 tokens under both encodings, so every token the newer encoding saved here, it saved on the Indonesian side. That is the finding a budget actually cares about. Anyone pricing an Indonesian workload off a 2023 article is budgeting for a penalty that has already been paid down by roughly half, on a model generation they are almost certainly not shipping any more.
Do not assume the direction is always down. Anthropic's token-counting documentation states that Claude 4.7 and later models use a newer tokenizer, that the same input text produces approximately 30 percent more tokens than on earlier models, and that you should recount rather than reuse counts measured against an earlier model. A tokenizer is a pricing variable, and it can move against you between two models from the same vendor.
Three of twenty Indonesian sentences were cheaper than the English
The average hides a range of 0.72x to 1.64x. The median pair ran 1.29x, above the corpus ratio, and three of the 20 Indonesian sentences cost fewer tokens than their English twin on o200k_base. These are the ends of that distribution.
| Sentence pair | EN tokens | ID tokens | Ratio |
|---|---|---|---|
| Payment made, invoice not yet received | 18 | 13 | 0.72x |
| Monthly stock count on the last working day | 17 | 16 | 0.94x |
| Free shipping above a rupiah threshold | 13 | 20 | 1.54x |
| Subscription renews unless cancelled in time | 14 | 23 | 1.64x |
The cheap end is Indonesian doing what it is good at: no articles, no tense inflection, and two English loanwords sitting in one short sentence. The expensive end is a policy sentence assembled out of affixed verbs - diperpanjang, membatalkannya, penagihan - which is precisely the register that terms of service, ERP notices and billing emails are written in. If your product's Indonesian is mostly formal notices, budget nearer the top of that range than the middle of it.
Run the script over your own message catalogue rather than mine. Twenty sentences is enough to disprove 2.5x and nowhere near enough to set a budget. Point it at the strings you actually send - the system prompt, the few-shot block, a month of real user turns - and the ratio you get belongs to your product instead of somebody else's corpus.
Claude and Gemini publish no offline tokenizer, so count over the wire
Everything above holds for OpenAI's encodings, because OpenAI is the vendor that publishes them: tiktoken ships the BPE vocabularies as open source, which is the only reason an offline count is possible at all. Anthropic and Google do not offer an equivalent, and the honest method there is the count-tokens endpoint - POST to the messages count_tokens path for Claude, models.countTokens for Gemini. Anthropic documents its endpoint as free to use, rate-limited separately from generation, and returning an estimate that can differ from the billed figure by a small amount.
# Anthropic publishes no offline tokenizer, so you count over the wire.
# The endpoint is free and rate-limited separately from generation.
curl https://api.anthropic.com/v1/messages/count_tokens \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5",
"messages": [{
"role": "user",
"content": "Langganan Anda akan diperpanjang secara otomatis."
}]
}'
# -> {"input_tokens": 14}
# Google is the same shape: models.countTokens runs the model's own tokenizer.
curl "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:countTokens?key=$GEMINI_API_KEY" \
-H "Content-Type: application/json" -X POST \
-d '{"contents":[{"parts":[{"text":"Langganan Anda akan diperpanjang otomatis."}]}]}'
# The model id is not decoration. Anthropic counts under the tokenizer of the
# model you pass, and its docs say Claude 4.7 and later produce about 30 percent
# more tokens for the same text than earlier models. A count is per model, not
# per vendor - and never per blog post.That distinction is bigger than it sounds. An offline tokenizer lets you count a million strings inside a test. A network endpoint turns the same language-ratio experiment into a rate-limited batch job tied to one specific model id, which you have to budget for and then repeat every time you change models - which is exactly what Anthropic's documentation tells you to do rather than reusing an older count.
What 23 percent is actually worth on a bill
Published list prices per million tokens, read from each vendor's own pricing page on 18 September 2026. They move, so check them rather than quoting this table back at anyone.
| Provider | Model | Input per 1M tokens | Output per 1M tokens |
|---|---|---|---|
| OpenAI | gpt-5 | $1.25 | $10.00 |
| Anthropic | Claude Sonnet 5 | $2.00 | $10.00 |
| Gemini 2.5 Flash | $0.30 | $2.50 |
Take a workload whose English version would bill one million input tokens. The Indonesian version bills 1,230,000 - an extra 230,000 tokens. That is 29 cents more on gpt-5, 46 cents more on Claude Sonnet 5, and 7 cents more on Gemini 2.5 Flash. On cl100k_base the same workload would have carried 520,000 extra tokens instead of 230,000, which is where the frightening numbers in older articles came from.
Output is where the penalty actually lands, because output costs at least five times input on every row of that table - the same 230,000 extra tokens are 2.30 dollars as gpt-5 output rather than 29 cents as input. And the comparison nobody makes is the one that puts it in proportion: input on Claude Sonnet 5 is 6.7 times input on Gemini 2.5 Flash. The language is 1.23x. Model choice and caching are the levers; the language is an adjustment on top of them.
Four levers, and what each one actually costs you
Three of these are worth doing. The fourth is the one people reach for first, and it is usually a false economy.
- Prompt caching, first and by a distance. Anthropic prices a cache read at 0.1x base input and a five-minute cache write at 1.25x, so a stable prefix pays for itself after a single read, and OpenAI lists cached input on gpt-5 at 0.125 dollars against 1.25 standard. The cost is discipline: the cached prefix has to be stable and long enough to justify a breakpoint, which means the system prompt and the few-shot block stop being things you edit casually.
- Trim the few-shot examples, because Indonesian examples carry the 1.23x on every single call while doing exactly the same job. The cost is behavioural: examples are what hold a model to your output format, so remove them against an eval rather than against a hunch, and expect to put at least one back.
- Write the system prompt in English and instruct the model to reply in Indonesian - but not for the reason usually given. I measured a matched pair of realistic support system prompts and got 61 tokens for English against 63 for Indonesian on o200k_base. Two tokens. The Indonesian version was code-switched, full of loanwords like support, booking and refund, which is how people genuinely write these. On cl100k_base the same pair was 61 against 85, so this advice was real in 2023 and is close to noise now.
Which leaves the one that sounds cleverest: translate the user's Indonesian into English, prompt in English, translate the answer back. You are adding an entire extra API call to save 23 percent of the input tokens on one call, and the extra call produces output tokens, which are the expensive kind. You also pay two round trips of latency inside a chat UI, and you lose the register. Indonesian marks formality in ways English simply does not - Anda against kamu, the distance between Mohon maaf and Maaf ya - and a round trip through English flattens all of it. The flattening is visible to the customer in a way a token count never is.
Translate-first earns its place in exactly one shape: a batch pipeline over documents, where nobody reads the Indonesian output, latency is free, and the translation itself runs on a cheap model. In a live support chat it costs money, time and tone at the same time.
What this measurement does not tell you
Four limits, all of them mine.
- Twenty pairs is 300 English tokens. That is enough to rule out 2.5x and nowhere near enough to be a language constant - treat 1.23x as an order of magnitude, not a coefficient.
- Register decides the answer. My corpus is formal product text. A code-switched support system prompt I measured separately came out at 1.03x on o200k_base, because half of it is English loanwords. Casual chat, regional informal Indonesian and mixed Indonesian-English input all behave differently again.
- These are OpenAI's encodings only. Nothing here transfers to Claude or Gemini, and Anthropic's own 30 percent tokenizer jump between model generations is proof that a ratio measured on one vendor is not a fact about the language.
- I measured text I wrote, not text a model generates. Output tokens are where the money is, and a model's Indonesian is not necessarily as compact as a careful human's.
None of that makes the exercise useless. It makes it a method rather than a number: the script is twenty lines, your corpus is already sitting in your repository, and the ratio it gives you is the only one that belongs in your spreadsheet.
The rule I use now
A language multiplier that arrives without a measurement attached is a rumour, and 2x to 3x is the loudest rumour in Indonesian AI costing. The real figure on a current OpenAI encoding was 1.23x for my corpus, it was 1.52x two model generations ago, and three sentences in twenty went the other way entirely. Measure your own strings against the model id you will actually ship, re-measure when you change it, and then go and spend the afternoon on prompt caching - where a cache read is priced at a tenth of base input, and the saving dwarfs anything the language ever cost you.
Sources and further reading
- OpenAI tiktoken - the open-source BPE tokeniser and its published encodings
- gpt-tokenizer - pure TypeScript implementation of OpenAI's encodings
- Claude API documentation - token counting and the count_tokens endpoint
- Gemini API reference - models.countTokens
- OpenAI API pricing - per-million-token rates
- Claude API pricing - model rates and prompt caching multipliers
- Gemini API pricing - paid tier rates

