← BlogNov 5, 20267 min readAI Development

How to Add AI to Your Next.js App Without Making It Slow or Expensive

Adding AI to a Next.js app is easy. Adding it so the app still feels fast, and the bill at the end of the month does not scare you, takes a bit more thought.

I have put AI into a few of my own projects now. Postlin drafts LinkedIn posts with Gemini. Latent runs image edits through Cloudinary's generative AI. Each time I ran into the same two problems: the AI part made the app feel slow, or it made me nervous about cost.

Here is what I do now.

1. Stream everything a person waits for

A model can take several seconds to finish an answer. If you wait for the whole thing before showing anything, the app feels broken. If you stream it, the first words appear almost straight away and the wait feels short.

With the AI SDK, streaming from a route handler is only a few lines:

ts
// app/api/chat/route.ts
import { streamText, convertToModelMessages, type UIMessage } from "ai";
import { google } from "@ai-sdk/google";

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: google(process.env.GEMINI_MODEL!),
    messages: await convertToModelMessages(messages),
    maxOutputTokens: 800,
  });

  return result.toUIMessageStreamResponse();
}

On the client, useChat from @ai-sdk/react talks to /api/chat by default and renders tokens as they arrive. Switching to OpenAI is one import and one line: openai("your-model") from @ai-sdk/openai instead of google(...). The rest of the code stays the same.

One note: the AI SDK moves fast and the helper names have changed between major versions. Check the docs for the version you install before you copy anything, including this.

2. Route handlers for streams, server actions for small jobs

I use both, for different things.

Route handlers are my default for anything streamed or called from many places. They give me a plain HTTP endpoint, I can set maxDuration, and I can put rate limiting and auth checks right at the top.

Server actions are great for short, one-shot AI tasks tied to a form. Things like "suggest a title for this post" or "summarise this note". You call a function, you get a value back, and you do not have to write fetch code.

ts
"use server";
import { generateText } from "ai";
import { google } from "@ai-sdk/google";

export async function suggestTitle(text: string) {
  const { text: title } = await generateText({
    model: google(process.env.GEMINI_MODEL!),
    prompt: `Suggest one short title for this post:\n\n${text.slice(0, 4000)}`,
    maxOutputTokens: 30,
  });
  return title.trim();
}

Notice the slice. Never send unlimited user text to a model. Input tokens cost money too.

3. Cache answers you will get asked again

A lot of AI calls are repeats. The same product description, the same "explain this error", the same summary of a page that has not changed. Paying for the same answer twice is waste.

For deterministic jobs I hash the input and cache the result:

ts
import { createHash } from "crypto";
import { redis } from "@/lib/redis";

export async function cachedSummary(input: string) {
  const key = "sum:" + createHash("sha256").update(input).digest("hex");
  const hit = await redis.get(key);
  if (hit) return hit;

  const summary = await summarise(input); // your generateText call
  await redis.set(key, summary, "EX", 60 * 60 * 24);
  return summary;
}

This only makes sense where the same input should give the same output. Do not cache a chat. Do cache summaries, classifications, tags and anything built from content that rarely changes.

Two other cheap wins:

  • Do the work once, not on every view. If an AI field is shown to many people, generate it when the content is saved and store it in the database. Do not call the model on every page load.
  • Keep the system prompt stable. Some providers cache repeated prompt prefixes and charge less for them. Put the fixed instructions first and the changing part last.

4. Rate limit before you call the model

An AI endpoint without a rate limit is an open tab on your card. One bug in a useEffect or one person with a script can burn through a lot of tokens quickly.

A simple fixed window per user is enough to start:

ts
export async function rateLimit(userId: string, limit = 20) {
  const key = `rl:${userId}:${Math.floor(Date.now() / 60000)}`;
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, 60);
  return count <= limit;
}

// at the top of the route handler
if (!(await rateLimit(session.user.id))) {
  return new Response("Too many requests", { status: 429 });
}

Check auth first, then the rate limit, then call the model. In that order.

5. Control cost on purpose

Cost is mostly a design choice. These are the ones that matter most for me:

  • Pick the smallest model that does the job. Use a fast, cheap model for drafting, tagging and summaries. Save the big model for the hard cases.
  • Cap output. Always set maxOutputTokens. A short answer is often a better answer anyway.
  • Trim input. Send only the context the model needs, not the whole document or chat history.
  • Log usage. streamText and generateText give you token usage when they finish. Save it per user and per feature so you know where the money goes.
ts
const result = streamText({
  model,
  messages,
  onFinish: async ({ usage }) => {
    await logUsage(userId, "chat", usage.inputTokens, usage.outputTokens);
  },
});
  • Or let users bring their own key. In Postlin, each person adds their own Gemini key, stored encrypted with AES-256-GCM. The app has no AI bill to pass on. It adds a small step to onboarding, but for a side project it removes the cost problem entirely.

6. Keep the AI off the critical path

The fastest AI call is the one the person does not wait for. If a job does not need to finish before the page responds, move it to the background. Generate tags after a save, write the summary after an upload, publish the post later.

Also, never trust the output blindly. If the model returns data your app will use, check it with a schema, for example with Zod, before anything touches it. If it does not match, reject it or ask again.

The short version

  • Stream anything a person waits for.
  • Use route handlers for streams and server actions for small one-shot tasks.
  • Cache repeat work and generate shared AI content once.
  • Rate limit every AI endpoint, after auth and before the model.
  • Choose small models, cap output, trim input and log usage.
  • Push slow jobs to the background and validate what comes back.

None of this is complicated. It is just easy to skip when you are excited that the AI part works. Do it from the first version and you will not have to fix it later.

If you are adding AI to a Next.js app and want a second pair of eyes, get in touch.

Keep reading

  1. Nov 5, 2026

    How I Built Postlin's Post Scheduler (and Where BullMQ Fits)

    Software Development
  2. Aug 17, 2026

    Instagram's Wordmark Finally Moved

    Design Strategy