Azure API Management Caching Strategies
Improving Performance with Response Caching
Introduction
Caching is one of the most effective ways to improve API performance and reduce backend load. Azure API Management (APIM) provides built-in caching capabilities that can dramatically reduce response times and API costs.
This comprehensive guide covers:
- Cache basics — How APIM caching works
- Built-in cache — Using APIM's internal cache
- Redis cache — Using Azure Redis for distributed caching
- Cache policies — Configuration options and examples
- Vary by — Controlling cache variations
- Best practices — Optimal caching strategies
How APIM Caching Works
Cache Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ APIM CACHING ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client │ │ Client │ │
│ │ 1 │ │ 2 │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ API Management Gateway │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Cache (Built-in or Redis) │ │ │
│ │ │ │ │ │
│ │ │ Key: /orders?status=active Value: {...} │ │ │
│ │ │ Key: /products Value: {...} │ │ │
│ │ │ TTL: 3600s Expires: 14:30 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └──────┬───────────────────────────────────────┬──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Backend API │ │ Backend API │ │
│ │ (first time) │ │ (not called) │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Cache Key Generation
APIM generates cache keys based on:
- URL path — The request path
- Query parameters — (if configured to include)
- Vary-by headers — Custom headers that differentiate cached responses
- Developer ID — (if vary-by-developer is enabled)
Built-in Cache Configuration
Basic Cache Policy
<outbound>
<!-- Check cache -->
<cache-lookup vary-by-developer="false">
<vary-by-query-parameter>page</vary-by-query-parameter>
</cache-lookup>
<!-- Store in cache -->
<cache-store duration="3600" />
</outbound>
Cache with Vary-by Headers
<outbound>
<cache-lookup vary-by-developer="false">
<!-- Cache separate versions for different Accept headers -->
<vary-by-header>Accept</vary-by-header>
<!-- Also vary by format -->
<vary-by-header>Accept-Language</vary-by-header>
</cache-lookup>
<!-- Cache for 1 hour -->
<cache-store duration="3600" />
</outbound>
Complete Cache Policy
<outbound>
<choose>
<!-- Only cache successful responses -->
<when condition="@(context.Response.StatusCode == 200)">
<cache-lookup vary-by-developer="true">
<vary-by-header>Accept</vary-by-header>
<vary-by-header>Accept-Encoding</vary-by-header>
<vary-by-query-parameter>version</vary-by-query-parameter>
</cache-lookup>
<cache-store duration="300" />
</when>
<otherwise>
<!-- Don't cache error responses -->
</otherwise>
</choose>
</outbound>
Azure Redis Cache
Configure Redis Cache
<outbound>
<!-- Lookup from Redis -->
<cache-lookup-from-redis
redis-cache-name="my-redis-cache"
redis-cache-connection-string="<connection-string-from-keyvault>" />
<!-- Store to Redis -->
<cache-store duration="7200" />
</outbound>
Redis Connection in App Settings
{
"CachedRedisConnectionString": {
"type": "securestring",
"value": "rediss://myredis.cache.windows.net:6380,password=xxx,ssl=true"
}
}
Redis vs Built-in Cache
| Feature | Built-in Cache | Redis Cache |
|---|---|---|
| Scope | Single APIM instance | Cross-instance |
| Capacity | Limited | Scalable |
| Persistence | In-memory | Durable |
| Cost | Included | Additional |
| Latency | Lower | Slightly higher |
Cache Key Variations
Vary by Developer
<outbound>
<!-- Separate cache per developer -->
<cache-lookup vary-by-developer="true">
<vary-by-header>Authorization</vary-by-header>
</cache-lookup>
<cache-store duration="1800" />
</outbound>
Vary by Query Parameters
<outbound>
<!-- Cache different versions for different page numbers -->
<cache-lookup>
<vary-by-query-parameter>page</vary-by-query-parameter>
<vary-by-query-parameter>size</vary-by-query-parameter>
<vary-by-query-parameter>sort</vary-by-query-parameter>
</cache-lookup>
<cache-store duration="300" />
</outbound>
Vary by Headers
<outbound>
<!-- Different cached response for different Accept types -->
<cache-lookup>
<vary-by-header>Accept</vary-by-header>
</cache-lookup>
<!-- JSON vs XML responses cached separately -->
<cache-store duration="600" />
</outbound>
Conditional Caching
Only Cache Specific Responses
<outbound>
<choose>
<!-- Cache GET requests only -->
<when condition="@(context.Request.Method == HttpMethod.Get)">
<!-- Only cache successful responses -->
<when condition="@(context.Response.StatusCode == 200)">
<cache-lookup vary-by-developer="false">
<vary-by-header>Accept</vary-by-header>
</cache-lookup>
<cache-store duration="300" />
</when>
</when>
<!-- Don't cache other methods -->
</choose>
</outbound>
Cache Based on Response Content
<outbound>
<choose>
<when condition="@{
var cacheControl = context.Response.Headers.GetValueOrDefault(\"Cache-Control\", \"\");
return cacheControl.Contains(\"no-cache\") == false;
}">
<cache-lookup />
<cache-store duration="600" />
</when>
</choose>
</outbound>
Cache Invalidation
Invalidate Cache on Update
<inbound>
<!-- Delete cache when products are updated -->
<choose>
<when condition="@(context.Request.Method == HttpMethod.Delete)">
<cache-remove-value key="products" />
</when>
<when condition="@(context.Request.Method == HttpMethod.Put ||
context.Request.Method == HttpMethod.Post)">
<cache-remove-value key="products" />
<cache-remove-value key="products-categories" />
</when>
</choose>
</outbound>
Wildcard Invalidation
<inbound>
<!-- Invalidate all product-related caches -->
<cache-remove>
<on-error>
<!-- Continue even if removal fails -->
</on-error>
</cache-remove>
</outbound>
Use Cases and Recommended Durations
Cache Duration Guidelines
| API Type | Example | Recommended Duration |
|---|---|---|
| Static data | Country codes, categories | 1-24 hours |
| Reference data | Product catalog, pricing | 5-60 minutes |
| User-specific | User profile, preferences | 5-30 minutes |
| Dynamic data | Stock levels, availability | 1-10 minutes |
| Search results | Complex queries | 1-5 minutes |
| Real-time | Live data, timestamps | Don't cache |
Examples by Use Case
<!-- Static reference data - long cache -->
<outbound>
<cache-lookup />
<cache-store duration="86400" /> <!-- 24 hours -->
</outbound>
<!-- User profile - medium cache -->
<outbound>
<cache-lookup vary-by-developer="true" />
<cache-store duration="1800" /> <!-- 30 minutes -->
</outbound>
<!-- Product catalog - short cache -->
<outbound>
<cache-lookup>
<vary-by-query-parameter>category</vary-by-query-parameter>
</cache-lookup>
<cache-store duration="300" /> <!-- 5 minutes -->
</outbound>
Best Practices
| Practice | Description |
|---|---|
| Cache GET requests only | POST/PUT/DELETE should modify data |
| Use vary-by appropriately | Don't over-segment cache |
| Set appropriate TTL | Balance freshness vs. performance |
| Cache errors briefly | Don't cache errors for long |
| Invalidate on updates | Clear cache when data changes |
| Monitor cache hit rate | Track cache effectiveness |
Monitoring Cache
# Get cache metrics
az monitor app-insights query \
--app my-apim-insights \
--query "requests | where name contains 'cache'"
Complete Example: Product API
<policies>
<inbound>
<!-- Add cache key header -->
<set-variable name="cacheKey" value="@(context.Request.Url.Path)" />
<!-- Invalidate on write -->
<choose>
<when condition="@(context.Request.Method == HttpMethod.Post ||
context.Request.Method == HttpMethod.Put ||
context.Request.Method == HttpMethod.Delete)">
<cache-remove>
<on-error>
<!-- Continue on error -->
</on-error>
</cache-remove>
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<choose>
<when condition="@(context.Request.Method == HttpMethod.Get &&
context.Response.StatusCode == 200)">
<!-- Vary by Accept header (JSON vs XML) -->
<cache-lookup vary-by-developer="false">
<vary-by-header>Accept</vary-by-header>
<vary-by-header>Accept-Language</vary-by-header>
<vary-by-query-parameter>category</vary-by-query-parameter>
</cache-lookup>
<!-- Cache for 5 minutes -->
<cache-store duration="300" />
</when>
</choose>
<!-- Add cache headers to response -->
<set-header name="X-Cache" exists-action="override">
<value>@(context.Variables.ContainsKey("CacheHit") ? "HIT" : "MISS")</value>
</set-header>
</outbound>
</policies>
Related Topics
- Rate Limiting — Throttling requests
- JWT Validation — Authentication
- Policy Engine — Advanced policies
Azure Integration Hub - Intermediate Level