# Alchemy Performance Tuning

> Optimize Alchemy SDK performance with caching, batching, and multi-chain parallelism. Use when reducing latency for blockchain queries, optimizing CU consumption, or scaling dApps for high request volumes. Trigger: "alchemy performance", "alchemy slow", "alchemy optimization", "alchemy caching", "alchemy batch requests".

- Skill: `gabrielmoreira/alchemy-performance-tuning` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/alchemy-performance-tuning`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/alchemy-performance-tuning/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/alchemy-performance-tuning

---

# Alchemy Performance Tuning

## Overview

Optimize a Web3 application's response time and provider use through
freshness-aware caching, bounded parallelism, batching, and real-time
subscriptions. Measure improvements against an approved baseline rather than
assuming fewer calls always preserves correct chain state.

## Performance Targets

| Operation | Target Latency | CU Cost |
|-----------|---------------|---------|
| `getBlockNumber` | < 50ms | 10 |
| `getBalance` | < 100ms | 19 |
| `getTokenBalances` | < 200ms | 50 |
| `getNftsForOwner` | < 300ms | 50 |
| `getAssetTransfers` | < 500ms | 150 |
| Multi-chain portfolio | < 2s | ~400 |

## Prerequisites

- A representative, non-sensitive benchmark workload with baseline latency,
  cache-hit, error-rate, and compute-unit measurements.
- An explicit freshness policy for balances, blocks, transfers, ownership, and
  metadata, approved by the product owner.
- Monitoring and a rollback flag that can disable cache, batching, or WebSocket
  changes if correctness or provider behavior regresses.

## Instructions

### Step 1: Response Caching with TTL

```typescript
// src/performance/cache.ts
import { Alchemy, Network } from 'alchemy-sdk';

class BlockchainCache {
  private store = new Map<string, { data: any; expiry: number }>();

  // Different TTLs for different data freshness needs
  private TTL: Record<string, number> = {
    blockNumber: 12000,     // 12s (~1 block)
    balance: 30000,         // 30s
    tokenBalances: 60000,   // 60s
    nftOwnership: 300000,   // 5 min (NFTs transfer less frequently)
    contractMetadata: 3600000, // 1 hour (rarely changes)
    tokenMetadata: 86400000,   // 24 hours (almost never changes)
  };

  async cached<T>(category: string, key: string, fetcher: () => Promise<T>): Promise<T> {
    const cacheKey = `${category}:${key}`;
    const entry = this.store.get(cacheKey);
    if (entry && entry.expiry > Date.now()) return entry.data;

    const data = await fetcher();
    this.store.set(cacheKey, { data, expiry: Date.now() + (this.TTL[category] || 30000) });
    return data;
  }

  invalidate(category: string): void {
    for (const key of this.store.keys()) {
      if (key.startsWith(`${category}:`)) this.store.delete(key);
    }
  }
}

const cache = new BlockchainCache();
export { cache };
```

### Step 2: Parallel Multi-Chain Fetching

```typescript
// src/performance/parallel-fetch.ts
import { Alchemy, Network } from 'alchemy-sdk';
import { cache } from './cache';

const CHAINS = [
  { name: 'ethereum', network: Network.ETH_MAINNET },
  { name: 'polygon', network: Network.MATIC_MAINNET },
  { name: 'arbitrum', network: Network.ARB_MAINNET },
  { name: 'base', network: Network.BASE_MAINNET },
];

async function multiChainBalance(address: string) {
  const results = await Promise.allSettled(
    CHAINS.map(chain =>
      cache.cached('balance', `${chain.name}:${address}`, async () => {
        const client = new Alchemy({ apiKey: process.env.ALCHEMY_API_KEY, network: chain.network });
        const bal = await client.core.getBalance(address);
        return { chain: chain.name, balance: (parseInt(bal.toString()) / 1e18).toFixed(6) };
      })
    )
  );

  return results
    .filter((r): r is PromiseFulfilledResult<any> => r.status === 'fulfilled')
    .map(r => r.value);
}
```

### Step 3: Batch NFT Metadata (Reduce CU)

```typescript
// src/performance/batch-nft.ts
import { Alchemy, Network } from 'alchemy-sdk';

const alchemy = new Alchemy({ apiKey: process.env.ALCHEMY_API_KEY, network: Network.ETH_MAINNET });

// SLOW: Individual calls = 50 CU each
// async function slowGetMetadata(tokens) {
//   return Promise.all(tokens.map(t => alchemy.nft.getNftMetadata(t.contract, t.tokenId)));
// }

// FAST: Batch call = 50 CU total for up to 100 tokens
async function fastGetMetadata(tokens: Array<{ contractAddress: string; tokenId: string }>) {
  return alchemy.nft.getNftMetadataBatch(tokens);
}
```

### Step 4: WebSocket for Real-Time Data

```typescript
// src/performance/realtime.ts
import { Alchemy, AlchemySubscription, Network } from 'alchemy-sdk';

const alchemy = new Alchemy({ apiKey: process.env.ALCHEMY_API_KEY, network: Network.ETH_MAINNET });

// Use WebSocket subscriptions instead of polling
function watchAddress(address: string, onActivity: (tx: any) => void) {
  alchemy.ws.on(
    {
      method: AlchemySubscription.PENDING_TRANSACTIONS,
      toAddress: address,
    },
    (tx) => onActivity(tx)
  );
}

// Auto-reconnect on disconnect
alchemy.ws.on('close', () => {
  console.log('WebSocket disconnected — reconnecting in 5s');
  setTimeout(() => alchemy.ws.connect(), 5000);
});
```

## Output

- TTL-based response cache matching data freshness requirements
- Parallel multi-chain fetching (4 chains in < 2s)
- Batch NFT metadata (100x CU reduction)
- WebSocket subscriptions replacing polling

## Examples

Benchmark a public test address across the four listed networks before and
after enabling the balance cache. Confirm the cached run reduces provider calls
while its displayed data never exceeds the approved 30-second freshness window,
and verify a single failed chain remains visibly unavailable rather than
silently omitted. Next, send a small synthetic NFT list through the batch path
and compare response count with individual calls. If cache age, error rate, or
WebSocket reconnect behavior violates the defined threshold, disable that
optimization using the rollback flag and investigate from aggregate metrics.

## Error Handling

| Failure | Response |
|---------|----------|
| Cache entry exceeds its freshness policy | Invalidate it and refresh from the provider before rendering a result. |
| One chain query fails | Preserve successful-chain results and surface an explicit unavailable state for the failed chain. |
| WebSocket repeatedly disconnects | Use bounded reconnect backoff, alert on sustained failure, and fall back to rate-limited polling. |
| Batch call partially fails | Keep successful results, retry only eligible failed items, and respect the provider limit. |

## Resources

- [Alchemy Compute Units](https://www.alchemy.com/docs/reference/compute-unit-costs)
- Alchemy WebSockets

## Next Steps

For cost optimization, see `alchemy-cost-tuning`.

