Prompt Caching mit ElastiCache beschleunigt Claude-Anfragen in Node.js 22
Wiederholte LLM-Anfragen kosten unnötig Zeit und Tokens. Ein Ansatz zeigt, wie sich Claude-Antworten in einem Redis-Cache mit ElastiCache Serverless zwischenspeichern und praktisch ohne Latenz ausliefern lassen.

LLMs sind schnell, aber jede wiederholte Anfrage kostet trotzdem Zeit und Tokens. Was wäre, wenn Sie die Antworten von Claude in einem Redis-Cache speichern und sofort ausliefern koennten? Dieser Beitrag zeigt, wie Sie ElastiCache Serverless in einen Latenz-freien Prompt-Cache fuer Ihre Node.js 22 App verwandeln.
Große Sprachmodelle (LLM) wie Claude beantworten einen Prompt, also den Text, den man ihnen schickt, indem sie ein gewaltiges neuronales Netzwerk durchlaufen. Auch wenn der Inferenzschritt nur einige hundert Millisekunden dauert, verursacht jeder Aufruf:
Stellen Sie sich ein LLM wie einen Koch vor, der fuer jede Bestellung ein individuelles Gericht von Grund auf zubereitet. Wenn ein Kunde immer wieder dasselbe Gericht bestellt, muss der Koch trotzdem jedes Mal von vorn beginnen, obwohl sich das Rezept nie aendert. Prompt Caching ist das Pendant zum Vorkochen des Gerichts und warmhalten, sodass die naechste Bestellung sofort serviert werden kann.
Wenn derselbe Nutzer oder ein anderer Nutzer dieselbe Frage stellt, koennen Sie den teuren Zubereitungsschritt ueberspringen und die zwischengespeicherte Antwort zurueckgeben. Der Leistungsgewinn wirkt wie geschenkt, aber nur wenn sich der Cache korrekt verhaelt. Ein falscher Cache-Schluessel oder ein veralteter Eintrag kann Ihren Nutzern die falsche Antwort liefern und die Logik Ihrer Anwendung still brechen.
Ganz klar formuliert: Speichern Sie das Ergebnis eines Prompts im Cache, nicht den Prompt selbst. Aendert sich der Prompt auch nur wenig, benoetigen Sie einen neuen Schluessel; sonst riskieren Sie, die falsche Antwort auszuliefern.
ElastiCache Serverless ist eine verwaltete, bedarfsgesteuerte Variante von Redis (oder Valkey), die automatisch skaliert. Sie erspart die Bereitstellung einer festen Anzahl von Knoten, hat aber ein anderes Preismodell:
# 1. Serverless Cache ueber die AWS Konsole oder CLI anlegen aws elasticache create-replication-group \ --replication-group-id my-cache \ --engine valkey \ --cache-node-type serverless \ --num-node-groups 1 \ --automatic-failover-enabled # 2. Endpoint notieren (z.B. my-cache.xxxxxx.use1.cache.amazonaws.com:6379) # 3. Die VPC Security Group zuweisen, die Ihre Lambda/EC2 Instanz nutzt Tipp: Aktivieren Sie Verschluesselung bei Uebertragung und im Ruhezustand; das verursacht vernachlaessigbare Latenz und haelt die Daten sicher.
Node.js 22 bringt einen experimentellen fetch-basierten Redis Client mit ( node:redis ). Er nutzt unter der Haube die standardmaessige fetch API, sodass Sie keine separate native Bindung wie ioredis benoetigen. Das haelt das Deployment klein (besonders bei Lambda) und funktioniert gut mit den in TypeScript eingebauten Typen.
npm i redis@^4.6 # Version mit Unterstuetzung fuer fetch-basierten Transport Minimaler Verbindungscode import { createClient } from ' redis '; // Der Endpoint aus der ElastiCache Konsole const REDIS_ENDPOINT = process. env. REDIS_ENDPOINT?? ' my-cache.xxxxxx.use1.cache.amazonaws.com:6379 '; // Client erstellen, der ueber TCP mit Redis spricht und intern fetch nutzt const redisClient = createClient ({ // url nutzt das Schema "redis://"; fetch uebernimmt den TCP Handshake url: `redis:// ${ REDIS_ENDPOINT } `, // Optional: fetch einen kurzen Timeout geben, um bei Netzwerkproblemen schnell zu scheitern socket: { // 2 Sekunden reichen fuer einen Aufruf innerhalb der VPC usually timeout: 2000, }, }); redisClient. on ( ' error ', ( err ) => { console. error ( ' Redis Verbindungsfehler: ', err ); }); await redisClient. connect (); // Gibt ein Promise zurueck; auf den Handshake warten Kernpunkt: Der fetch-basierte Client ist nur eine weitere Moeglichkeit, mit Redis zu sprechen; der Rest Ihres Codes (GET/SET) bleibt gleich.
Nun, da wir einen Redis Client haben, koennen wir die zentrale Funktion bauen:
Ein roher Prompt-String kann lang sein, Zeilenumbrueche enthalten oder sich nur in Leerzeichen unterscheiden. Den gesamten Prompt als Schluessel zu speichern, bringt Redis zu Mehraufwand und erzeugt viele fast identische Schluessel. Ein Hash (z.B. SHA-256) wirkt wie ein Fingerabdruck: Zwei identische Prompts erzeugen denselben kurzen Bezeichner, waehrend jede Aenderung (selbst ein ueberzaehliges Leerzeichen) einen anderen Fingerabdruck liefert.
import { createHash } from ' crypto '; // eingebautes Node Modul zum Hashen import { fetch } from ' node-fetch '; // Node 22 hat natives fetch; Import nur fuer Typhinweise import { redisClient } from './redisClient '; // der oben verbundene Client // Claude API Details (durch echten Endpoint und Schluessel ersetzen) const CLAUDE_ENDPOINT = ' https://api.anthropic.com/v1/messages '; const CLAUDE_API_KEY = process. env. CLAUDE_API_KEY!; // Helferfunktion: Prompt in einen kurzen, stabilen Schluessel umwandeln function promptToCacheKey ( prompt: string ): string { // SHA-256 liefert einen 64 Zeichen langen Hex String; Praefix vermeidet Kollisionen mit anderen Caches const hash = createHash ( ' sha256 ' ). update ( prompt. trim ()). digest ( ' hex ' ); return `claude:prompt: ${ hash } `; } // Hauptfunktion: zuerst Cache versuchen, sonst Claude aufrufen, Ergebnis cachen export async function getClaudeAnswer ( prompt: string ): Promise < any > { // Schritt 1: Cache Schluessel ableiten const cacheKey = promptToCacheKey ( prompt ); // Schritt 2: Versuch, aus Redis zu lesen const cached = await redisClient. get ( cacheKey ); if ( cached ) { // Cache Treffer: JSON parsen und sofort zurueckgeben console. log ( ' Cache Treffer fuer Schluessel ', cacheKey ); return JSON. parse ( cached ); } // Schritt 3: Kein Cache Eintrag, Claude aufrufen console. log ( ' Cache Miss, rufe Claude auf... ' ); const response = await fetch ( CLAUDE_ENDPOINT, { method: ' POST ', headers: { ' Content-Type ': ' application/json ', ' x-api-key ': CLAUDE_API_KEY, }, body: JSON. stringify ({ model: ' claude-3-5-sonnet-20241001 ', max_tokens: 1024, messages: [{ role: ' user ', content: prompt }], }), }); if (! response. ok ) { // Fehler ausgeben; in einer echten Anwendung waere Retry oder Fallback moeglich const errorBody = await response. text (); throw new Error ( `Claude API Fehler ${ response. status }: ${ errorBody } ` ); } const json = await response. json (); // Claude liefert JSON mit der Antwort // Schritt 4: Ergebnis mit 5 Minuten TTL in Redis speichern // JSON.stringify stellt sicher, dass ein String gespeichert wird (Redis speichert Bytes) await redisClient. set ( cacheKey, JSON. stringify ( json ), { EX: 300, // EX setzt Ablaufzeit in Sekunden; 300 s = 5 min }); // Schritt 5: Frische Antwort zurueckgeben return json; } Erlaeuterung der einzelnen Bloecke
Tipp: Halten Sie die TTL kurz fuer sich rasch aendernde Wissensgebiete (Nachrichten, Wetter). Fuer statische FAQs koennen Sie eine laengere TTL setzen oder den Eintrag sogar dauerhaft speichern.
Wenn Sie den Prompt direkt mit anderen Bezeichnern (z.B. der User ID) ohne Trennzeichen verketten, koennten "user42:hello" und "user4:2hello" kollidieren. Verwenden Sie immer ein klares Trennzeichen oder noch besser: hashen Sie jede Komponente separat und verbinden Sie sie mit einem Doppelpunkt.
function makeKey ( userId: string, prompt: string ): string { const promptHash = createHash ( ' sha256 ' ). update ( prompt ). digest ( ' hex ' ); return `claude:user: ${ userId }:prompt: ${ promptHash } `; } 2. Leerzeichen und Gross-/Kleinschreibung Ein Prompt, der sich nur durch zusaetzliche Leerzeichen oder andere Gross-/Kleinschreibung unterscheidet, erzeugt einen anderen Hash und fuehrt zu einem Cache Miss. Entscheiden Sie, ob der Cache case-insensitiv und leerzeichen-agnostisch sein soll, und normalisieren Sie den Text vor dem Hashen.
const normalised = prompt. replace ( / \s +/g, ' ' ). trim (). toLowerCase (); 3. TTL (time to live) Dimensionierung Eine zu lang gesetzte TTL kann veraltete Informationen ausliefern; eine zu kurze macht den Cache sinnlos. Ein uebliches Muster ist stale-while-revalidate: Die zwischengespeicherte Wert wird auch dann ausgeliefert, wenn er einige Sekunden alt ist, waehrend er im Hintergrund aktualisiert wird. Dafuer benoetigt man einen zweiten Schluessel (z.B.:lock ) oder einen Hintergrundjob, was den Rahmen dieses einfachen Tutorials sprengt, aber erwaehnenswert ist.
Ganz klar formuliert: Betrachten Sie die TTL wie das Mindesthaltbarkeitsdatum auf Milch. Kurzlebige Daten (wie Nachrichten) benoetigen ein frisches Datum; stabile Daten (wie eine Produktbeschreibung) koennen laenger liegenbleiben.
Redis speichert Bytes; wir serialisieren JSON ueblicherweise mit JSON.stringify. Speichern Sie das rohe Objekt (ohne Stringifizierung) in einem Client, der automatisch eine BSON Konvertierung durchfuehrt, koennte binaere Daten entstehen, die JSON.parse nicht lesen kann. Bleiben Sie durchgehend bei einem einzigen Format.
// FALSCH: Binaerdaten und Strings mischen await redisClient. set ( key, Buffer. from ( JSON. stringify ( obj ))); // RICHTIG: Immer einen String speichern await redisClient. set ( key, JSON. stringify ( obj )); 5. Valkey spezifische Besonderheiten Der SET Befehl von Valkey akzeptiert dieselbe EX Option, aber einige fortgeschrittene Features wie SETEX verhalten sich leicht anders. Falls Sie einen Befehl benoetigen, der fehlschlaegt, werfen Sie einen Blick in die Valkey Dokumentation oder testen Sie lokal mit einem Valkey Docker Image.
Fuehren Sie diesen Code in einer AWS Lambda Funktion innerhalb einer VPC aus, muss die erste Anfrage nach einer Inaktivitaetsphase eine ENI (elastic network interface) anbinden. Das kostet 1 bis 2 Sekunden, bevor der Redis Client ueberhaupt starten kann. Abmildern laesst sich das durch:
Kernpunkt: Die groesste versteckte Latenz liegt nicht im Cache Lookup, sondern im Netzwerkgeruest, das Ihre Funktion in die VPC bringt.
Was Sie nach der Lektuere dieses Leitfadens im Kopf behalten sollten
Wenn Sie diese Schritte befolgen, verwandeln Sie wiederholte Claude-Aufrufe in sofortige Look-ups, sparen Geld, reduzieren die Latenz und sorgen dafür, dass die Antworten Ihrer Anwendung zuverlässig bleiben. Viel Spaß beim Caching!
Dieser Artikel wurde mit Unterstützung eines KI-Systems erstellt, nämlich Groq (GPT OSS 120B).
Veröffentlicht: 2026-10-09 · Schwerpunkt: ElastiCache
Alle Codeblöcke sind als korrekt und ausführbar gedacht, bitte überprüfen Sie sie jedoch vor dem Einsatz in der Produktivumgebung anhand der offiziellen Dokumentation der genannten Tools.
Haben Sie einen Fehler gefunden? Hinterlassen Sie einen Kommentar, Korrekturen sind immer willkommen.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
LLMs are fast, but every repeated request still costs time and tokens. What if you could memoize Claude’s answers in a Redis cache and serve them instantly? This post shows how to turn ElastiCache Serverless into a zero‑latency prompt cache for your Node.js 22 app.
Large language models (LLM) such as Claude answer a prompt – the text you send them – by running a massive neural network. Even though the inference step takes only a few hundred milliseconds, each call:
Think of an LLM like a chef who prepares a custom dish from scratch for every order. If a customer keeps ordering the same dish, the chef still has to start from the beginning each time, even though the recipe never changes. Prompt caching is the equivalent of pre‑cooking the dish and keeping it warm so the next order is served instantly.
When the same user (or a different user) asks the same question, you can skip the expensive cooking step and hand back the cached answer. The performance win feels free, but only if the cache behaves correctly. A mismatched cache key or an outdated entry can feed the wrong answer back to your users, silently breaking your app’s logic.
In plain English: Cache the result of a prompt, not the prompt itself. If the prompt changes even a little, you need a new key; otherwise you risk serving the wrong answer.
ElastiCache Serverless is a managed, on‑demand version of Redis (or Valkey) that scales automatically. It removes the need to provision a fixed number of nodes, but the pricing model is different:
# 1. Create a Serverless cache via the AWS console or CLI aws elasticache create-replication-group \ --replication-group-id my-cache \ --engine valkey \ --cache-node-type serverless \ --num-node-groups 1 \ --automatic-failover-enabled # 2. Note the endpoint (e.g., my-cache.xxxxxx.use1.cache.amazonaws.com:6379) # 3. Attach the VPC security group that your Lambda/EC2 uses Enter fullscreen mode Exit fullscreen mode Tip: Enable encryption‑in‑transit and at‑rest; it adds negligible latency and keeps data safe.
Node.js 22 ships with an experimental fetch‑based Redis client ( node:redis ). It uses the standard fetch API under the hood, so you don’t need a separate native binding like ioredis. This keeps the deployment footprint small (especially for Lambda) and works nicely with TypeScript’s built‑in types.
npm i redis@^4.6 # version that supports fetch‑based transport Enter fullscreen mode Exit fullscreen mode Minimal connection code import { createClient } from ' redis '; // The endpoint we got from the ElastiCache console const REDIS_ENDPOINT = process. env. REDIS_ENDPOINT?? ' my-cache.xxxxxx.use1.cache.amazonaws.com:6379 '; // Build a client that talks to Redis over TCP using fetch internally const redisClient = createClient ({ // url uses the "redis://" scheme; fetch will handle the TCP handshake url: `redis:// ${ REDIS_ENDPOINT } `, // Optional: give fetch a short timeout so we fail fast on network hiccups socket: { // 2 seconds is usually enough for an in‑VPC call timeout: 2000, }, }); redisClient. on ( ' error ', ( err ) => { console. error ( ' Redis connection error: ', err ); }); await redisClient. connect (); // Returns a promise; wait for the handshake Enter fullscreen mode Exit fullscreen mode Key takeaway: The fetch‑based client is just another way to talk to Redis; the rest of your code (GET/SET) stays the same.
Now that we have a Redis client, we can build the core function:
A raw prompt string can be long, contain line breaks, or differ only in whitespace. Storing the entire prompt as a key makes Redis do extra work and creates many near‑duplicate keys. A hash (e.g., SHA‑256) acts like a fingerprint: two identical prompts produce the same short identifier, while any change (even a stray space) yields a different fingerprint.
import { createHash } from ' crypto '; // built‑in Node module for hashing import { fetch } from ' node-fetch '; // Node 22 has native fetch; import only for type hinting import { redisClient } from './redisClient '; // the client we connected above // Claude API details (replace with your real endpoint and key) const CLAUDE_ENDPOINT = ' https://api.anthropic.com/v1/messages '; const CLAUDE_API_KEY = process. env. CLAUDE_API_KEY!; // Helper: turn a prompt into a short, stable key function promptToCacheKey ( prompt: string ): string { // SHA‑256 gives a 64‑char hex string; we prefix to avoid collisions with other caches const hash = createHash ( ' sha256 ' ). update ( prompt. trim ()). digest ( ' hex ' ); return `claude:prompt: ${ hash } `; } // Main function: tries cache first, falls back to Claude, then caches result export async function getClaudeAnswer ( prompt: string ): Promise < any > { // Step 1 – derive cache key const cacheKey = promptToCacheKey ( prompt ); // Step 2 – attempt to read from Redis const cached = await redisClient. get ( cacheKey ); if ( cached ) { // Cache hit: parse JSON and return immediately console. log ( ' Cache hit for key ', cacheKey ); return JSON. parse ( cached ); } // Step 3 – no cache entry, call Claude console. log ( ' Cache miss, calling Claude... ' ); const response = await fetch ( CLAUDE_ENDPOINT, { method: ' POST ', headers: { ' Content-Type ': ' application/json ', ' x-api-key ': CLAUDE_API_KEY, }, body: JSON. stringify ({ model: ' claude-3-5-sonnet-20241001 ', max_tokens: 1024, messages: [{ role: ' user ', content: prompt }], }), }); if (! response. ok ) { // Surface the error; in a real app you might retry or fallback const errorBody = await response. text (); throw new Error ( `Claude API error ${ response. status }: ${ errorBody } ` ); } const json = await response. json (); // Claude returns JSON with the answer // Step 4 – store the result in Redis with a 5‑minute TTL // JSON.stringify makes sure we store a string (Redis stores bytes) await redisClient. set ( cacheKey, JSON. stringify ( json ), { EX: 300, // EX sets expiration in seconds; 300 s = 5 min }); // Step 5 – return the fresh answer return json; } Enter fullscreen mode Exit fullscreen mode Explanation of each block
Tip: Keep the TTL short for rapidly changing knowledge domains (news, weather). For static FAQs you can set a longer TTL or even store forever.
If you concatenate the prompt directly with other identifiers (e.g., user ID) without a separator, "user42:hello" and "user4:2hello" could clash. Always use a clear delimiter or, better, hash each component separately and join them with a colon.
function makeKey ( userId: string, prompt: string ): string { const promptHash = createHash ( ' sha256 ' ). update ( prompt ). digest ( ' hex ' ); return `claude:user: ${ userId }:prompt: ${ promptHash } `; } Enter fullscreen mode Exit fullscreen mode 2. Whitespace and case sensitivity A prompt that differs only by extra spaces or different capitalisation will produce a different hash, causing a cache miss. Decide whether you want the cache to be case‑insensitive and whitespace‑agnostic, then normalise before hashing.
const normalised = prompt. replace ( / \s +/g, ' ' ). trim (). toLowerCase (); Enter fullscreen mode Exit fullscreen mode 3. TTL (time‑to‑live) sizing Setting a TTL that’s too long can serve outdated information; too short defeats the purpose of caching. A common pattern is stale‑while‑revalidate: serve the cached value even if it’s a few seconds old while you refresh it in the background. Implementing that requires a second key (e.g.,:lock ) or a background job, which is beyond this simple tutorial but worth noting.
In plain English: Think of TTL like the expiration date on milk. Short‑lived data (like news) needs a fresh date; stable data (like a product description) can sit longer.
Redis stores bytes; we usually stringify JSON with JSON.stringify. If you store the raw object (without stringifying) in a client that automatically performs BSON conversion, you could end up with binary data that JSON.parse can’t read. Stick to a single format throughout the app.
// WRONG – mixing binary and string await redisClient. set ( key, Buffer. from ( JSON. stringify ( obj ))); Enter fullscreen mode Exit fullscreen mode // RIGHT – always store a string await redisClient. set ( key, JSON. stringify ( obj )); Enter fullscreen mode Exit fullscreen mode 5. Valkey‑specific quirks Valkey’s SET command accepts the same EX option, but some advanced features like SETEX behave slightly differently. If you ever need to use a command that fails, check the Valkey docs or test locally with a Valkey Docker image.
When you run this code inside an AWS Lambda that lives in a VPC, the first request after a period of inactivity must attach an ENI (elastic network interface). That adds 1‑2 seconds before the Redis client can even start. Mitigate by:
Key takeaway: The biggest hidden latency isn’t the cache lookup; it’s the network plumbing that brings your function into the VPC.
What you should remember after reading this guide
By following these steps you’ll turn repeated Claude calls into instant look‑ups, saving money, cutting latency, and keeping your app’s answers reliable. Happy caching!
This article was written with the help of an AI system — Groq (GPT OSS 120B).
Published: 2026-10-09 · Primary focus: ElastiCache
All code blocks are intended to be correct and runnable, but please verify them against the official docs for the tools mentioned before using in production.
Find an error? Drop a comment — corrections are always welcome.