7 Ways to Make Your API Faster Maneshwar, a developer building LiveReview, an AI code review tool, shares seven techniques for making APIs faster, emphasizing that optimization should follow measurement and profiling. The techniques include short-TTL caching, connection pooling, and addressing serverless connection exhaustion, with code examples and practical advice. Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. Star us https://github.com/HexmosTech/LiveReview/ to help devs discover the project, give it a try, and share your feedback to help improve the product. Your API is slow. You know it is slow because somebody in the product channel posted a screenshot of a spinner with the caption "is this normal". So you open the codebase, and within ninety seconds you have three theories, two refactors planned, and a strong urge to swap the JSON library. Stop. Put the keyboard down. Optimization is not step one. Optimization is step four, after measurement, after confirmation, and after you have found something that is actually slow. Every optimization in this post buys you speed with complexity . Cache invalidation, connection pool tuning, pagination cursors, async log buffers, none of that is free. You pay for it forever, in every future debugging session. So the entry fee is a profile. Load test the endpoint, look at where the time actually goes, and only then pick a technique from the list below. The number of times I have watched somebody spend a week optimizing serialization for an endpoint whose real problem was one unindexed query is not a small number. Measure. Confirm. Then optimize. In that order, every time. Okay. Assume you measured. Here are the seven things worth reaching for. Caching is the highest leverage trick on this list, because the fastest database query is the one you never send. The shape is simple. An expensive computation runs once, the result goes into Redis or Memcached, and the next N callers asking the same question get the stored answer. The catch is that people think caching is a big architectural commitment. It usually is not. python def get top products category: str : key = f"top products:{category}" if hit := redis.get key : return json.loads hit result = db.query expensive top products category redis.setex key, 30, json.dumps result thirty seconds. that is it. return result Look at that TTL. Thirty seconds. That feels almost insultingly short, and it is exactly the point. If an endpoint takes 400ms and gets hit 200 times a minute, a thirty second cache removes something like 99% of those database hits, and nobody downstream ever notices data that is half a minute stale. Short TTLs are underrated because they give you most of the win with almost none of the invalidation pain. You are not maintaining a cache, you are just refusing to answer the same question 200 times in a row. Where it gets genuinely hard is when the data must be fresh, and then you are in invalidation territory, which is famously one of the two hard things in computer science https://martinfowler.com/bliki/TwoHardThings.html . Start with the boring TTL version. Graduate to invalidation only when the TTL version is provably wrong for your use case. Opening a database connection is not free. There is a TCP handshake, usually a TLS handshake, then authentication, then session setup. You can easily spend more time saying hello to Postgres than you spend querying it. Connection pooling keeps a set of connections open and hands them out. Your request borrows one, runs its query, and gives it back. Most frameworks do this by default and you never think about it. Which is fine, right up until the day you go serverless. Serverless breaks the assumption underneath pooling. Each function instance is its own little process with its own little pool, and the platform will happily spin up 500 of them during a traffic spike. Now your database, which is configured for maybe 100 connections, is getting introduced to 500 strangers at once. Postgres in particular does not degrade gracefully here. It forks a process per connection, so connection exhaustion is not a slowdown, it is a wall. That is the entire reason AWS RDS Proxy https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html exists, along with pgbouncer and friends. They sit between your ephemeral functions and your very much non-ephemeral database, and multiplex a small pool of real connections across a large number of callers. flowchart LR subgraph Serverless F1 Fn instance 1 F2 Fn instance 2 F3 Fn instance ...500 end F1 -- P Connection Proxy F2 -- P F3 -- P P -- |small, reused pool| DB Postgres F1 -.- |without a proxy| DB F2 -.- |500 handshakes| DB F3 -.- |database says no| DB classDef fn fill: 6ea8ff,stroke: 2f5fb8,color: 1a1a1a classDef proxy fill: 5ee6c8,stroke: 1f9c86,color: 1a1a1a classDef db fill: ff9a5c,stroke: c0632c,color: 1a1a1a class F1,F2,F3 fn class P proxy class DB db This is my favourite one, because the N+1 query is almost always the result of writing nicer code. You have posts. Each post has comments. So you write the obvious thing: posts = db.query "SELECT FROM posts LIMIT 20" for post in posts: one extra round trip. every. single. time. post.comments = db.query "SELECT FROM comments WHERE post id = %s", post.id That is 21 queries to render one page. Twenty of them are identical in shape and differ only in an integer. On your laptop the database is a localhost away, each query costs 0.2ms, and the whole loop finishes before you can blink. In production the database is across a network, each round trip costs 3ms, and you have just spent 60ms doing nothing but waiting. Scale that page to 200 posts and you have an endpoint that is somehow slow without a single slow query in the logs. That is what makes N+1 so nasty. Every individual query looks fine. The slow query log has nothing to say. Only the count is wrong. The fix is to stop asking one at a time: posts = db.query "SELECT FROM posts LIMIT 20" ids = p.id for p in posts rows = db.query "SELECT FROM comments WHERE post id = ANY %s ", ids by post = defaultdict list for row in rows: by post row.post id .append row for post in posts: post.comments = by post post.id Two queries. Constant, regardless of how many posts you fetch. If you are on an ORM, this is what select related and prefetch related in Django, joinedload in SQLAlchemy, and include in Prisma exist for. The tooling is there, it is just off by default, because the ORM cannot know whether you wanted the related rows. The single most useful habit here is to log your query count per request in development. An endpoint that fires 47 queries will tell on itself immediately. Somewhere in every codebase there is an endpoint that started life returning 12 records and now returns 40,000, because the table grew and nobody revisited the handler. The database has to fetch it. Your app has to serialize it. The network has to ship it. The client has to parse it, and then render precisely the first twenty of them. Pagination is the fix and everyone knows it. What everyone does not know is that LIMIT 20 OFFSET 100000 is not actually fast. OFFSET does not skip work. The database still walks all 100,000 rows and throws them away before handing you twenty. Deep pages get linearly slower, and your "optimization" quietly becomes the new bottleneck. Cursor based pagination avoids this by asking the question differently. Instead of "give me page 5000", you ask "give me the twenty rows after this one": -- offset: gets slower the deeper you go SELECT FROM events ORDER BY id LIMIT 20 OFFSET 100000; -- cursor: uses the index, same cost on page 1 and page 5000 SELECT FROM events WHERE id 100000 ORDER BY id LIMIT 20; The second one is an index seek. It costs the same at any depth. The tradeoff is that you lose random access to page numbers, which is why offset pagination survives in admin panels and cursor pagination is what you find in Stripe's API https://docs.stripe.com/api/pagination and every infinite scroll feed you have ever used. Once the data is in memory, something has to turn it into JSON, and that something is running on your CPU for every single response. For small payloads this is noise. For an endpoint returning a few thousand objects, serialization can genuinely become the dominant cost, and the profiler will point right at it. The good news is that this is the cheapest fix on the entire list, because it is usually a library swap. In Python, orjson is meaningfully faster than the standard library json . In Node, the JSON serializer is native but schema based approaches like fast-json-stringify https://github.com/fastify/fast-json-stringify beat it by knowing the shape in advance. Serializers that get told the schema upfront can skip all the runtime type sniffing. But please, actually profile first. Swapping serializers on an endpoint that spends 95% of its time in the database is a lovely way to spend an afternoon achieving nothing. JSON compresses beautifully, because JSON is mostly repeated key names and whitespace. Compression ratios of 5x to 10x on API responses are completely normal. That is 5x to 10x less data crossing the network, which matters enormously for anyone on mobile, and matters for everyone once payloads get big. gzip is the safe default that every client on earth supports. Brotli https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding generally compresses smaller at comparable speed and is supported by every modern browser, so it is worth enabling where you can. Two things to keep in mind. Compression costs CPU. You are trading processor time for network time, which is nearly always a good trade, but it is still a trade. Small responses can genuinely come out slower once you add the compression overhead, so most servers have a minimum size threshold, and you should leave it on. And you very likely should not be doing this yourself. Cloudflare, Fastly and friends will compress at the edge for you, which moves the CPU cost off your servers entirely and applies it uniformly to everything you serve. If you are already behind a CDN, this optimization is a checkbox. This one is last for a reason. Most services should not care. But in a high throughput path, writing a log line is a syscall, and if that write is synchronous and blocking, your request thread is sitting there waiting on a disk or a network socket while doing nothing useful. Async logging fixes this by making the request thread's job trivial. It drops the log entry into an in memory ring buffer and moves on immediately. A separate thread drains the buffer and does the actual writing. The request path goes from "wait for the write" to "append to a queue". php flowchart LR R Request thread -- |append, microseconds| B In-memory buffer B -- W Logger thread W -- D Disk / log service R -- RESP Response sent C{App crashes