A 700 ms render with 4 ms of compiling, and where the rest went
Once Typst compiles an invoice in milliseconds, the network and the database decide how fast the API feels. We measured each leg from India and moved the compute to match.
- performance
- infrastructure
- multi-region
In an earlier post we showed a two-page invoice compiling in about 4 ms. A customer calling the API from Bengaluru does not see 4 ms. On 5 November 2026, over an already-open connection from India, a complete render took about 700 ms. This post is about the other 695 ms, because they taught us more about building a fast API than the compiler did.
Measuring each leg
A synchronous render passes through four stages: the customer's request reaches our edge, the edge authenticates and checks quota against the database, it calls the compile service, and the PDF streams back. We timed each stage separately from India on 3 and 5 November.
| Leg | Time | Notes |
|---|---|---|
| Customer to edge | 200 to 500 ms | Some Indian ISPs route to an edge in Marseille, France |
| Edge to database | about 90 ms per round trip | The database's primary is most likely in Singapore |
| Edge to compile service | one round trip plus the PDF transfer | Mumbai to Mumbai after the change below |
| Compiling | 4 ms for two pages, 305 ms for 30, 3.6 s for 300 | Typst 0.15.1, 2 GB |
The database's own query time was under a millisecond. The 90 ms was entirely the distance between the server running our code and the server holding the data.
The first surprise: where the edge is
Edge networks promise to run your code near the user. From India that is mostly true, but not always. Several large Indian ISPs hand traffic to the network in Europe, so a request from Hyderabad was being served from Marseille. Our code then called a database in Singapore and a compile service in Mumbai, which turned one render into two intercontinental round trips before any compiling happened.
The fix was to stop running next to the visitor and start running next to our backends. The API now has a fixed placement in Mumbai. The visitor's request still enters the network wherever their ISP sends it, but from that point it travels once over the provider's backbone to Mumbai, and every subsequent hop is short.
The second surprise: the number of database trips
Before we looked closely, a synchronous render made nine sequential database round trips: look up the API key, load the project, load the template version, reserve quota, record the job, and so on, each waiting for the previous one. At 90 ms each that was over 800 ms on its own.
We cut it to three by batching writes into a single statement group and by letting independent reads run in parallel. The remaining three are the floor for a design that reserves quota before compiling and commits it after, which is the design that lets us never charge for a failed render.
Trying other locations
Once the API was in Mumbai we tried moving it. Singapore, next to the database, made a render only about 50 ms faster, because the trip to the compile service in Mumbai got longer by almost the same amount. Hong Kong and Tokyo were slower on both counts. The API stays in Mumbai beside the compiler, and the work that remains is making fewer database trips, not moving closer to the database.
What a render looks like now
Over an open connection from India, about 700 ms, of which:
- roughly 300 ms is the customer's own network path to our edge,
- about 270 ms is three database round trips,
- one trip to the compile service, including transferring the PDF,
- and about 4 ms of compiling.
For callers that do not need the PDF in the response, the asynchronous mode replies after two database round trips and delivers the finished file to a webhook, which removes the compile and transfer time from the request entirely.
Lessons for anyone building on an edge network
The database and the network shape latency more than compute does. A fast compiler moved the bottleneck, it did not remove it.
Place code next to its data, not next to its users, when every request needs the data. Edge placement is right for cached and static responses, and wrong for a request that must talk to a single primary database three times.
Count round trips, not query time. A sub-millisecond query is irrelevant when the wire costs 90 ms. Batching and parallelism are worth more than indexes here.
Multi-region is the next step, and it is why every project on PageRush already has a region and every job records the one it rendered in. Adding a second region means deploying the compile service and storage there, not rewriting the API, and for customers it means their documents compile and stay near them.