{"slug": "7-ways-to-make-your-api-faster", "title": "7 Ways to Make Your API Faster", "summary": "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.", "body_md": "*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.*\n\nYour API is slow.\n\nYou know it is slow because somebody in the product channel posted a screenshot of a spinner with the caption \"is this normal\".\n\nSo you open the codebase, and within ninety seconds you have three theories, two refactors planned, and a strong urge to swap the JSON library.\n\nStop.\n\nPut the keyboard down.\n\nOptimization is not step one. Optimization is step four, after measurement, after confirmation, and after you have found something that is actually slow.\n\nEvery optimization in this post buys you speed with **complexity**. \n\nCache invalidation, connection pool tuning, pagination cursors, async log buffers, none of that is free. You pay for it forever, in every future debugging session.\n\nSo the entry fee is a profile.\n\nLoad test the endpoint, look at where the time actually goes, and only then pick a technique from the list below.\n\nThe 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.\n\nMeasure. Confirm. Then optimize. In that order, every time.\n\nOkay. Assume you measured. Here are the seven things worth reaching for.\n\nCaching is the highest leverage trick on this list, because the fastest database query is the one you never send.\n\nThe 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.\n\nThe catch is that people think caching is a big architectural commitment. It usually is not.\n\n``` python\ndef get_top_products(category: str):\n    key = f\"top_products:{category}\"\n    if hit := redis.get(key):\n        return json.loads(hit)\n\n    result = db.query_expensive_top_products(category)\n    redis.setex(key, 30, json.dumps(result))   # thirty seconds. that is it.\n    return result\n```\n\nLook at that TTL. Thirty seconds.\n\nThat feels almost insultingly short, and it is exactly the point.\n\nIf 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.\n\nShort TTLs are underrated because they give you most of the win with almost none of the invalidation pain.\n\nYou are not maintaining a cache, you are just refusing to answer the same question 200 times in a row.\n\nWhere 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).\n\nStart with the boring TTL version. Graduate to invalidation only when the TTL version is provably wrong for your use case.\n\nOpening a database connection is not free.\n\nThere is a TCP handshake, usually a TLS handshake, then authentication, then session setup.\n\nYou can easily spend more time saying hello to Postgres than you spend querying it.\n\nConnection pooling keeps a set of connections open and hands them out. Your request borrows one, runs its query, and gives it back.\n\nMost frameworks do this by default and you never think about it. Which is fine, right up until the day you go serverless.\n\nServerless 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.\n\nNow your database, which is configured for maybe 100 connections, is getting introduced to 500 strangers at once.\n\nPostgres in particular does not degrade gracefully here. It forks a process per connection, so connection exhaustion is not a slowdown, it is a wall.\n\nThat is the entire reason [AWS RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html) exists, along with pgbouncer and friends. \n\nThey 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.\n\n```\nflowchart LR\n    subgraph Serverless\n      F1[Fn instance 1]\n      F2[Fn instance 2]\n      F3[Fn instance ...500]\n    end\n\n    F1 --> P[Connection Proxy]\n    F2 --> P\n    F3 --> P\n    P -->|small, reused pool| DB[(Postgres)]\n\n    F1 -.->|without a proxy| DB\n    F2 -.->|500 handshakes| DB\n    F3 -.->|database says no| DB\n\n    classDef fn fill:#6ea8ff,stroke:#2f5fb8,color:#1a1a1a\n    classDef proxy fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a\n    classDef db fill:#ff9a5c,stroke:#c0632c,color:#1a1a1a\n\n    class F1,F2,F3 fn\n    class P proxy\n    class DB db\n```\n\nThis is my favourite one, because the N+1 query is almost always the result of writing *nicer* code.\n\nYou have posts. Each post has comments. So you write the obvious thing:\n\n```\nposts = db.query(\"SELECT * FROM posts LIMIT 20\")\n\nfor post in posts:\n    # one extra round trip. every. single. time.\n    post.comments = db.query(\"SELECT * FROM comments WHERE post_id = %s\", post.id)\n```\n\nThat is 21 queries to render one page. Twenty of them are identical in shape and differ only in an integer.\n\nOn your laptop the database is a localhost away, each query costs 0.2ms, and the whole loop finishes before you can blink.\n\nIn production the database is across a network, each round trip costs 3ms, and you have just spent 60ms doing nothing but waiting.\n\nScale that page to 200 posts and you have an endpoint that is somehow slow without a single slow query in the logs.\n\nThat 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.\n\nThe fix is to stop asking one at a time:\n\n```\nposts = db.query(\"SELECT * FROM posts LIMIT 20\")\nids   = [p.id for p in posts]\n\nrows = db.query(\"SELECT * FROM comments WHERE post_id = ANY(%s)\", ids)\n\nby_post = defaultdict(list)\nfor row in rows:\n    by_post[row.post_id].append(row)\n\nfor post in posts:\n    post.comments = by_post[post.id]\n```\n\nTwo queries. Constant, regardless of how many posts you fetch.\n\nIf you are on an ORM, this is what `select_related` and `prefetch_related` in Django, `joinedload` in SQLAlchemy, and `include` in Prisma exist for. \n\nThe tooling is there, it is just off by default, because the ORM cannot know whether you wanted the related rows.\n\nThe 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.\n\nSomewhere 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.\n\nThe database has to fetch it. Your app has to serialize it. The network has to ship it.\n\nThe client has to parse it, and then render precisely the first twenty of them.\n\nPagination is the fix and everyone knows it. What everyone does not know is that `LIMIT 20 OFFSET 100000` is not actually fast.\n\n`OFFSET` does not skip work. The database still walks all 100,000 rows and throws them away before handing you twenty. \n\nDeep pages get linearly slower, and your \"optimization\" quietly becomes the new bottleneck.\n\nCursor 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\":\n\n```\n-- offset:  gets slower the deeper you go\nSELECT * FROM events ORDER BY id LIMIT 20 OFFSET 100000;\n\n-- cursor:  uses the index, same cost on page 1 and page 5000\nSELECT * FROM events WHERE id > 100000 ORDER BY id LIMIT 20;\n```\n\nThe second one is an index seek. It costs the same at any depth.\n\nThe 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.\n\nOnce the data is in memory, something has to turn it into JSON, and that something is running on your CPU for every single response.\n\nFor 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.\n\nThe 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`. \n\nIn 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.\n\nBut 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.\n\nJSON compresses beautifully, because JSON is mostly repeated key names and whitespace. Compression ratios of 5x to 10x on API responses are completely normal.\n\nThat is 5x to 10x less data crossing the network, which matters enormously for anyone on mobile, and matters for everyone once payloads get big.\n\ngzip 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.\n\nTwo things to keep in mind.\n\nCompression costs CPU. You are trading processor time for network time, which is nearly always a good trade, but it is still a trade.\n\nSmall 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.\n\nAnd 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.\n\nIf you are already behind a CDN, this optimization is a checkbox.\n\nThis one is last for a reason. Most services should not care.\n\nBut 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.\n\nAsync 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.\n\nThe request path goes from \"wait for the write\" to \"append to a queue\".\n\n``` php\nflowchart LR\n    R[Request thread] -->|append, microseconds| B[In-memory buffer]\n    B --> W[Logger thread]\n    W --> D[(Disk / log service)]\n\n    R --> RESP[Response sent]\n\n    C{App crashes<br/>before flush?}\n    B -.-> C\n    C -->|yes| L[Buffered logs lost]\n    C -->|no| D\n\n    classDef thread fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a\n    classDef buf fill:#6ea8ff,stroke:#2f5fb8,color:#1a1a1a\n    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a\n    classDef bad fill:#ff9a5c,stroke:#c0632c,color:#1a1a1a\n\n    class R,W thread\n    class B,RESP buf\n    class C decision\n    class L,D bad\n```\n\nThat dotted branch is the whole tradeoff, and you should stare at it before enabling this.\n\nAnything sitting in the buffer when the process dies is gone. Which means the logs describing the crash are exactly the logs most likely to be lost.\n\nThat is a genuinely bad trade for audit logs, payment records, or anything you would need in an incident review.\n\nIt is a perfectly fine trade for high volume access logs where losing the last few hundred lines costs you nothing.\n\nPick per log stream, not per application.\n\nLook at the seven again and notice how they cluster.\n\nCaching, pooling and N+1 are all about **not talking to the database**, whether by skipping the question, skipping the handshake, or asking once instead of twenty times.\n\nPagination, serialization and compression are all about **moving less data**, at the query, at the CPU, and on the wire.\n\nAsync logging is about **getting out of the request path**, which is the same idea as background jobs, applied to something small.\n\nNone of them are exotic. All of them are boring, well understood, and sitting one library call away.\n\nThe hard part was never knowing the techniques. The hard part is having the discipline to find out which one your endpoint actually needs, instead of applying all seven and calling it architecture.\n\nProfile first. Fix the thing the profile points at. Then go do something more interesting.\n\nYour team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.\n\nI'm building **LiveReview**, a blast-radius aware AI code review built for your business-critical systems.\n\nInstead of presenting every diff with equal emphasis, **LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.**\n\nSpend code review effort where business risk is highest — not spread evenly across every diff.\n\n⭐ Star it on GitHub:\n\nLiveReview is an AI code reviewer that scores every hunk of a diff by **blast radius**: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.\n\n*LiveReview's Blast Radius & Review Priority scoring, live in the diff viewer.*\n\n| The exact math, not a black box | Visualize blast radius at a glance | Every factor that feeds the score | \n|---|---|---|\n\n**Here's the goal:**\n\n**Click below to try LiveReview with your codebase:**", "url": "https://wpnews.pro/news/7-ways-to-make-your-api-faster", "canonical_source": "https://dev.to/lovestaco/7-ways-to-make-your-api-faster-4020", "published_at": "2026-09-08 16:42:29+00:00", "updated_at": "2026-09-08 16:55:58.054088+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Maneshwar", "LiveReview", "Redis", "Memcached", "Postgres", "AWS RDS Proxy"], "alternates": {"html": "https://wpnews.pro/news/7-ways-to-make-your-api-faster", "markdown": "https://wpnews.pro/news/7-ways-to-make-your-api-faster.md", "text": "https://wpnews.pro/news/7-ways-to-make-your-api-faster.txt", "jsonld": "https://wpnews.pro/news/7-ways-to-make-your-api-faster.jsonld"}}