Skip to content

Caching Strategy ​

Companion to the README. Every cache layer in the package, with the cost math that justifies each.


The five cache layers ​

LayerWhereTTL defaultKey shape
Model cataloguecore-ai.bedrock.cache.models_ttl3600 sbedrock_ai_models_*
CloudWatch usagecore-ai.bedrock.cache.usage_ttl900 sbedrock_ai_usage_*
AWS Pricing APIcore-ai.bedrock.cache.pricing_ttl86400 sbedrock_ai_pricing_*
Response cache (v2.1.0)core-ai.cache.response_ttl0 (off)bedrock_ai_response_{sha256(...)}
Embedding cache (v2.1.0)core-ai.bedrock.cache.embedding_ttl (fallback core-ai.cache.embedding_ttl)604800 s (7 d)bedrock_ai_embeddings_{sha256(...)}
Prompt cache markers (v2.1.0)core-ai.bedrock.prompt_caching.pointsupstream 300 s (max 3600)injected into Converse payload

The package does NOT cache:

  • The daily / monthly cost counters (bedrock_ai_daily_cost_…, bedrock_ai_monthly_cost_…) — those are the spend ledger, not a memo.
  • Streaming responses (stream() / converseStream()). The response-cache layer only memoises invoke() / converse(); streamed results are returned as an assembled array and never written to the response cache.
  • Authentication state.

1. Prompt cache markers (cachePoint) ​

The biggest single lever. Bedrock charges full rate for the first call's prefix, then ~10% for any subsequent call's prefix that matches a cachePoint anchor.

Anchors ​

AnchorWhere it landsWhen useful
systemAfter the system-prompt content blocksStatic system prompts reused across calls. The most common case.
last_userAfter the last user-message content blocksA user message + cached assistant reply prefix pattern (re-used multi-turn headers).

Up to 4 checkpoints per Converse request (Bedrock limit). 2 anchors is the typical configuration.

Configuration ​

php
'bedrock' => [
    'prompt_caching' => [
        'points'     => ['system', 'last_user'],
        'ttl_seconds' => 300, // 5 min, max 3600 (1 h)
    ],
],

Or via env:

dotenv
BEDROCK_PROMPT_CACHE_POINTS=system,last_user
BEDROCK_PROMPT_CACHE_TTL=600

Caveat: unsupported models ​

Bedrock returns a 400 if a cachePoint is placed on a model that doesn't support prompt caching. If you switch models and start seeing 400 cachePoint is not supported for this model, drop the cache-points from config.

Cost math ​

Suppose a Claude 3.5 Sonnet call has:

  • 600-token system prompt (static).
  • 100-token user message (varies).
  • 200-token output.
ScenarioInput bill
No cache700 input × $0.003/1k = $0.0021
cachePoint on system, identical system within TTL100 fresh × $0.003/1k + 600 cached × 10% × $0.003/1k = $0.00048

~77% off the input-token bill on cached calls. The output bill is unaffected.


2. Response cache (core-ai.cache.response_ttl) ​

In-process memoisation. The manager caches invoke() and converse() results in Laravel's default cache store. Bypassing is trivial (change temperature by 0.001 or maxTokens by 1).

When to enable ​

  • Re-ingestion of structured data (CSV → JSON).
  • Templated replies (summarise this, classify this support ticket).
  • Anything (model, system, user, max, temp)-keyed that repeats inside the TTL window.

When NOT to enable ​

  • Live chat with rolling history (cache miss every turn = wasted compute).
  • Streaming responses.

Configuration ​

php
'cache' => [
    'response_ttl' => 3600, // memoise for 1 hour
],

Cache key ​

php
hash('sha256', "{$modelId}|{$systemPrompt}|{$userMessage}|{$maxTokens}|{$temperature}");

prefixed with bedrock_ai_response_.

Bypass ​

Vary any of the five input parts. To force-recompute the same content on demand:

php
use Illuminate\Support\Facades\Cache;
Cache::forget('bedrock_ai_response_' . hash('sha256', "model|sys|user|1024|0.2"));

3. Embedding cache (core-ai.bedrock.cache.embedding_ttl, fallback core-ai.cache.embedding_ttl) ​

The manager first looks up core-ai.bedrock.cache.embedding_ttl, then falls back to core-ai.cache.embedding_ttl (the latter is the universal default). BedrockManager::embed() memoise per-text vectors:

php
$vectors = $manager->embed('amazon.titan-embed-text-v2:0', $corpus, dimensions: 1024);

The cache key is sha256(model|dimensions|text). Changing any of those resets the cache.

When to invalidate ​

You almost never need to. If the source text changes for one item, the new SHA differs and a new row is written. The 7-day default is the time-to-cold; you can stretch it (per-platform override first, universal fallback):

php
// config/core-ai.php
'bedrock' => [
    'cache' => [
        'embedding_ttl' => 30 * 86400, // 30 days (this key wins)
    ],
],

// OR, if you don't want to set per-platform:
'cache' => [
    'embedding_ttl' => 30 * 86400, // 30 days (universal fallback)
],

4. Idempotency-Key (v2.1.0+) ​

BedrockClient::invoke() derives an Idempotency-Key HTTP header from sha256(modelId|system|user) and attaches it on the Bearer-mode HTTP path:

Idempotency-Key: <sha256 of modelId|system|user>

Bedrock uses the header to deduplicate retries. A network-blip retry (or withRetry() exhaustion) returns the same cached response instead of double-billing. From your app's perspective, two requests with the same key are idempotent.

Compute the same key in your code ​

php
$key = app(BedrockManager::class)->idempotencyKey($modelId, $systemPrompt.$userMessage);
// 'bedrock_ai-<sha256 hash>'

Use it when storing invocation metadata so retries can be traced by key.


5. Retry-After honouring (v2.1.0+) ​

When the upstream returns 429: Too Many Requests, the Bearer-mode HTTP path captures the Retry-After header before throwing. HasRetryLogic::withRetry() prefers the hint over the exponential backoff:

ScenarioEffective wait
Exponential path (no hint)2 s, 4 s, 8 s (per BEDROCK_RETRY_DELAY)
With Retry-After hintOften 5-30 s — the actual upstream cooldown

Recovery is usually much faster with hints. If the upstream doesn't include Retry-After, the exponential path is the fallback.


6. Putting it all together ​

For a hot path that uses a 600-token static system prompt and 1k variable user messages per hour:

LeverWithout v2.1.0With v2.1.0 (all on)
Input cost per call (avg 700 tokens)$0.0021$0.00048 + 0 % retries
Network blip retrydouble-billeddeduplicated upstream
Rate-limit backoff14 s exponential5-30 s upstream cooldown
Repeated prompt (e.g. ingest)full rate0 cost

For ingestion of a 100k-row corpus:

LeverImpact
core-ai.cache.embedding_ttl = 7 daysOne-shot cost; re-runs are free

7. Cache store ​

The package uses Laravel's default cache store. For production:

dotenv
CACHE_STORE=redis

Row sizes:

  • Response cache: a few KB per row.
  • Embedding cache: vector bytes (1.5 KB for 1024-dim float embeddings).
  • Model catalogue: 1-50 KB per listFoundationModels response.

Redis is recommended for prod. The file driver works for local dev. APCu is fast but evicted on php-fpm restart — avoid for the embedding cache.


Worked example: 1M calls/day ​

Inputs:

  • 1M chat calls/day.
  • 600-token static system prompt.
  • 100-token average user message.
  • 200-token average output.
  • 1 % network-blip retry rate.
LayerPer-call cost (avg)Daily
Base (no v2.1.0)input $0.0021, retry +1 % × $0.0021$2.10 / call × 1.01M ≈ $2,121/day
cachePoint on systeminput $0.00048, no retry cost$0.48 / call × 1M ≈ $480/day
response_ttl = 1h on templated calls (20 %)0 cost on 20 % of calls(-$0.0001 × 200k) ≈ -$20/day
Retry-After honouring30 % faster recovery (latency win, not cost)(latency)

Net: ~$1.6k/day saved on a 1M-call hot path, + ~30 % faster recovery. Multiplied by 30 days = ~$48k/month. For 10 hot paths: ~$580k/year.

Numbers use Claude 3.5 Sonnet's published $0.003/1k input rate and 10 % cached rate. Plug your own rates in.


Caveats and pitfalls ​

  • In-memory cache: the package does NOT use per-process memory. All caches go through Laravel's cache facade. Multi-process safety is automatic.
  • Content-hash determinism: the SHA256 hash is a function of $systemPrompt concatenated with $userMessage. Whitespace, casing, and Unicode all matter — Claude.\n and Claude. hash differently.
  • Cache-busting for a fast iteration loop: if you're testing prompts, set cache.response_ttl = 0 or change temperature between iterations.
  • Memory pressure: if the response cache holds 100k entries, your Redis or file cache will swell. Set a sensible TTL.

Released under the MIT License.