import { config } from '#app/config.server.ts';
import { GlideClient } from '@valkey/valkey-glide';
import Redis from 'ioredis';
import { createClient, createClientPool } from 'redis';
/**
* Benchmarks Redis vs Valkey client performance.
*
* Run with:
* ```
* node --expose-gc --import tsx app/bin/benchmark-redis.ts
* ```
*/
const maybeGc = () => {
if (global.gc) {
global.gc();
}
};
const parseRedisUrl = (
url: string,
): {
host: string;
port: number;
} => {
const parsed = new URL(url);
return {
host: parsed.hostname,
port: parsed.port ? Number(parsed.port) : 6_379,
};
};
type BenchmarkResult = {
avgMs: number;
maxMs: number;
minMs: number;
name: string;
opsPerSec: number;
totalMs: number;
};
const runBenchmark = async (
name: string,
iterations: number,
fn: () => Promise<void>,
): Promise<BenchmarkResult> => {
const times: number[] = [];
// Warmup
for (let i = 0; i < Math.min(100, iterations / 10); i++) {
await fn();
}
const start = performance.now();
for (let i = 0; i < iterations; i++) {
const iterStart = performance.now();
await fn();
times.push(performance.now() - iterStart);
}
const totalMs = performance.now() - start;
return {
avgMs: times.reduce((a, b) => a + b, 0) / times.length,
maxMs: Math.max(...times),
minMs: Math.min(...times),
name,
opsPerSec: Math.round((iterations / totalMs) * 1_000),
totalMs,
};
};
const formatResult = (result: BenchmarkResult): string => {
return `${result.name}: ${result.opsPerSec.toLocaleString()} ops/sec (avg: ${result.avgMs.toFixed(3)}ms, min: ${result.minMs.toFixed(3)}ms, max: ${result.maxMs.toFixed(3)}ms)`;
};
const main = async () => {
const { host, port } = parseRedisUrl(config.REDIS_DSN);
console.log(`Connecting to Redis at ${host}:${port}\n`);
// Initialize clients
const ioredisClient = new Redis({ host, port });
const ioredisAutoPipelineClient = new Redis({
enableAutoPipelining: true,
host,
port,
});
const redisClient = createClient({ url: `redis://${host}:${port}` });
await redisClient.connect();
const redisResp3Client = createClient({
RESP: 3,
url: `redis://${host}:${port}`,
});
await redisResp3Client.connect();
const redisPoolClient = createClientPool(
{ url: `redis://${host}:${port}` },
{ maximum: 5, minimum: 5 },
);
await redisPoolClient.connect();
const glideClient = await GlideClient.createClient({
addresses: [{ host, port }],
clientName: 'benchmark',
});
const iterations = 10_000;
const testKey = 'benchmark:string';
const hashKey = 'benchmark:hash';
const counterKey = 'benchmark:counter';
const pipelineKeyPrefix = 'benchmark:pipeline';
const concurrentKeyPrefix = 'benchmark:concurrent';
const testValue = 'hello world';
const largeValue = 'x'.repeat(10_000);
console.log(
`Running benchmarks with ${iterations.toLocaleString()} iterations each\n`,
);
// ===================
// SEQUENTIAL BENCHMARKS
// These run one operation at a time - tests raw client overhead
// ===================
maybeGc();
// SET benchmarks
console.log('=== SET (sequential) ===');
console.log(
formatResult(
await runBenchmark('ioredis', iterations, async () => {
await ioredisClient.set(testKey, testValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis', iterations, async () => {
await redisClient.set(testKey, testValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis (RESP3)', iterations, async () => {
await redisResp3Client.set(testKey, testValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', iterations, async () => {
await glideClient.set(testKey, testValue);
}),
),
);
maybeGc();
// GET benchmarks
console.log('\n=== GET (sequential) ===');
console.log(
formatResult(
await runBenchmark('ioredis', iterations, async () => {
await ioredisClient.get(testKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis', iterations, async () => {
await redisClient.get(testKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis (RESP3)', iterations, async () => {
await redisResp3Client.get(testKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', iterations, async () => {
await glideClient.get(testKey);
}),
),
);
maybeGc();
// Large value SET benchmarks
console.log('\n=== SET 10KB (sequential) ===');
console.log(
formatResult(
await runBenchmark('ioredis', iterations, async () => {
await ioredisClient.set(testKey, largeValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis', iterations, async () => {
await redisClient.set(testKey, largeValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis (RESP3)', iterations, async () => {
await redisResp3Client.set(testKey, largeValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', iterations, async () => {
await glideClient.set(testKey, largeValue);
}),
),
);
maybeGc();
// Large value GET benchmarks
console.log('\n=== GET 10KB (sequential) ===');
console.log(
formatResult(
await runBenchmark('ioredis', iterations, async () => {
await ioredisClient.get(testKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis', iterations, async () => {
await redisClient.get(testKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis (RESP3)', iterations, async () => {
await redisResp3Client.get(testKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', iterations, async () => {
await glideClient.get(testKey);
}),
),
);
maybeGc();
// HSET benchmarks
console.log('\n=== HSET (sequential) ===');
console.log(
formatResult(
await runBenchmark('ioredis', iterations, async () => {
await ioredisClient.hset(hashKey, 'field', testValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis', iterations, async () => {
await redisClient.hSet(hashKey, 'field', testValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis (RESP3)', iterations, async () => {
await redisResp3Client.hSet(hashKey, 'field', testValue);
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', iterations, async () => {
await glideClient.hset(hashKey, { field: testValue });
}),
),
);
maybeGc();
// HGET benchmarks
console.log('\n=== HGET (sequential) ===');
console.log(
formatResult(
await runBenchmark('ioredis', iterations, async () => {
await ioredisClient.hget(hashKey, 'field');
}),
),
);
console.log(
formatResult(
await runBenchmark('redis', iterations, async () => {
await redisClient.hGet(hashKey, 'field');
}),
),
);
console.log(
formatResult(
await runBenchmark('redis (RESP3)', iterations, async () => {
await redisResp3Client.hGet(hashKey, 'field');
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', iterations, async () => {
await glideClient.hget(hashKey, 'field');
}),
),
);
maybeGc();
// INCR benchmarks
console.log('\n=== INCR (sequential) ===');
await ioredisClient.set(counterKey, '0');
console.log(
formatResult(
await runBenchmark('ioredis', iterations, async () => {
await ioredisClient.incr(counterKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis', iterations, async () => {
await redisClient.incr(counterKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('redis (RESP3)', iterations, async () => {
await redisResp3Client.incr(counterKey);
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', iterations, async () => {
await glideClient.incr(counterKey);
}),
),
);
// ===================
// PIPELINE/TRANSACTION BENCHMARKS
// Note: ioredis pipeline() vs redis multi() are NOT equivalent:
// - pipeline(): Batches commands, no atomicity (faster)
// - multi(): Transaction with MULTI/EXEC atomicity (slower)
// redis v4+ lacks a direct pipeline equivalent
// ===================
maybeGc();
const pipelineSize = 100;
const pipelineIterations = iterations / pipelineSize;
console.log(
`\n=== Pipeline/Transaction (${pipelineSize} SETs per batch) ===`,
);
// ioredis pipeline - batches without atomicity
console.log(
formatResult(
await runBenchmark(
'ioredis pipeline (no atomicity)',
pipelineIterations,
async () => {
const pipeline = ioredisClient.pipeline();
for (let i = 0; i < pipelineSize; i++) {
pipeline.set(`${pipelineKeyPrefix}:${i}`, testValue);
}
await pipeline.exec();
},
),
),
);
// ioredis multi - transaction with atomicity (for fair comparison with redis)
console.log(
formatResult(
await runBenchmark(
'ioredis multi (atomic)',
pipelineIterations,
async () => {
const multi = ioredisClient.multi();
for (let i = 0; i < pipelineSize; i++) {
multi.set(`${pipelineKeyPrefix}:${i}`, testValue);
}
await multi.exec();
},
),
),
);
// redis multi - transaction with atomicity
console.log(
formatResult(
await runBenchmark(
'redis multi (atomic)',
pipelineIterations,
async () => {
const multi = redisClient.multi();
for (let i = 0; i < pipelineSize; i++) {
multi.set(`${pipelineKeyPrefix}:${i}`, testValue);
}
await multi.exec();
},
),
),
);
// redis RESP3 multi - transaction with atomicity
console.log(
formatResult(
await runBenchmark(
'redis (RESP3) multi (atomic)',
pipelineIterations,
async () => {
const multi = redisResp3Client.multi();
for (let i = 0; i < pipelineSize; i++) {
multi.set(`${pipelineKeyPrefix}:${i}`, testValue);
}
await multi.exec();
},
),
),
);
// valkey-glide doesn't have pipeline
console.log(
formatResult(
await runBenchmark(
'valkey-glide (no pipeline)',
pipelineIterations,
async () => {
for (let i = 0; i < pipelineSize; i++) {
await glideClient.set(`${pipelineKeyPrefix}:${i}`, testValue);
}
},
),
),
);
// ===================
// CONCURRENT BENCHMARKS
// Multiple operations in flight simultaneously - this is where
// auto-pipelining and connection pools provide benefits
// ===================
maybeGc();
const concurrentSize = 100;
const concurrentIterations = iterations / concurrentSize;
console.log(`\n=== Concurrent (${concurrentSize} parallel SETs) ===`);
// Single client - commands queue behind each other
console.log(
formatResult(
await runBenchmark('ioredis', concurrentIterations, async () => {
await Promise.all(
Array.from(
{ length: concurrentSize },
async (_, i) =>
await ioredisClient.set(`${concurrentKeyPrefix}:${i}`, testValue),
),
);
}),
),
);
// Auto-pipeline batches concurrent commands automatically
console.log(
formatResult(
await runBenchmark(
'ioredis (auto-pipeline)',
concurrentIterations,
async () => {
await Promise.all(
Array.from(
{ length: concurrentSize },
async (_, i) =>
await ioredisAutoPipelineClient.set(
`${concurrentKeyPrefix}:${i}`,
testValue,
),
),
);
},
),
),
);
// Single client
console.log(
formatResult(
await runBenchmark('redis', concurrentIterations, async () => {
await Promise.all(
Array.from({ length: concurrentSize }, (_, i) =>
redisClient.set(`${concurrentKeyPrefix}:${i}`, testValue),
),
);
}),
),
);
// Single client with RESP3
console.log(
formatResult(
await runBenchmark('redis (RESP3)', concurrentIterations, async () => {
await Promise.all(
Array.from({ length: concurrentSize }, (_, i) =>
redisResp3Client.set(`${concurrentKeyPrefix}:${i}`, testValue),
),
);
}),
),
);
// Pool distributes across 5 connections
console.log(
formatResult(
await runBenchmark('redis (pool)', concurrentIterations, async () => {
await Promise.all(
Array.from({ length: concurrentSize }, (_, i) =>
redisPoolClient.set(`${concurrentKeyPrefix}:${i}`, testValue),
),
);
}),
),
);
console.log(
formatResult(
await runBenchmark('valkey-glide', concurrentIterations, async () => {
await Promise.all(
Array.from({ length: concurrentSize }, (_, i) =>
glideClient.set(`${concurrentKeyPrefix}:${i}`, testValue),
),
);
}),
),
);
// Cleanup
console.log('\nCleaning up...');
await ioredisClient.del(testKey);
await ioredisClient.del(hashKey);
await ioredisClient.del(counterKey);
for (let i = 0; i < pipelineSize; i++) {
await ioredisClient.del(`${pipelineKeyPrefix}:${i}`);
}
for (let i = 0; i < concurrentSize; i++) {
await ioredisClient.del(`${concurrentKeyPrefix}:${i}`);
}
console.log('Done!');
};
main().catch(console.error);