Connection Sharing Guide
Overview
When an application uses both the AMQP client (for publishing) and worker (for consuming), amqp-contract automatically shares a single connection between them following RabbitMQ best practices. This guide explains how automatic connection sharing works.
Why Share Connections?
According to RabbitMQ best practices:
- Connections are expensive: TCP connection, TLS handshake, authentication, and heartbeat overhead
- Channels are lightweight: Multiplexed over a single connection
- Best practice: Share one connection, use multiple channels
Benefits
- Resource Efficiency: One TCP connection instead of two
- Reduced Overhead: Single authentication and heartbeat loop
- Better Scalability: Lower connection count in large deployments
- Cost Savings: ~50ms startup time improvement, ~5-10MB memory savings per service
Usage
Automatic Connection Sharing (Recommended)
Connection sharing is completely automatic when you use the same URLs:
import { TypedAmqpClient } from "@amqp-contract/client";
import { TypedAmqpWorker } from "@amqp-contract/worker";
import { contract } from "./contract";
// 1. Create client - automatically creates connection
const clientResult = await TypedAmqpClient.create({
contract,
urls: ["amqp://localhost"], // ← Just provide URLs
connectionOptions: {
heartbeatIntervalInSeconds: 30,
},
});
if (clientResult.isError()) {
throw clientResult.error;
}
const client = clientResult.value;
// 2. Create worker - automatically reuses the same connection!
const workerResult = await TypedAmqpWorker.create({
contract,
urls: ["amqp://localhost"], // ← Same URLs = automatic sharing
handlers: {
processOrder: async (message) => {
console.log("Processing order:", message.orderId);
// Can publish from within consumer
const publishResult = await client.publish("orderProcessed", {
orderId: message.orderId,
status: "completed",
});
publishResult.match({
Ok: () => console.log("Order processed event published"),
Error: (error) => console.error("Failed to publish:", error),
});
},
},
});
if (workerResult.isError()) {
throw workerResult.error;
}
const worker = workerResult.value;
// Both client and worker automatically share a single connection! ✅
// Result: 1 connection, 2 channels
// No manual connection management needed!Lifecycle Management
With automatic connection sharing, lifecycle management is simple:
// Create client and worker
const client = (
await TypedAmqpClient.create({
contract,
urls: ["amqp://localhost"],
})
).get();
const worker = (
await TypedAmqpWorker.create({
contract,
urls: ["amqp://localhost"],
handlers: {
/* ... */
},
})
).get();
// Close components when done
// 1. Close worker first (stops consuming)
await worker.close();
// 2. Close client (stops publishing)
await client.close();
// The shared connection is managed automatically by the singletonImportant: Each client and worker closes its own channel. When all clients/workers using a shared connection are closed, the underlying connection is automatically closed and cleaned up by the internal singleton's reference counting.
How Connection Sharing Works
When you create multiple clients or workers with the same URLs and connection options, amqp-contract automatically reuses the same underlying connection:
// ✅ Automatically shares a single connection
const client = await TypedAmqpClient.create({
contract,
urls: ["amqp://localhost"], // ← URLs match
});
const worker = await TypedAmqpWorker.create({
contract,
urls: ["amqp://localhost"], // ← URLs match = shared connection
handlers: {
/* ... */
},
});
// Result: 1 connection, 2 channels ✅
// - Less resource usage
// - Less network overhead
// - Faster startup
// - Zero manual connection managementThe singleton ConnectionManagerSingleton internally caches connections based on URLs and connection options. When you create a new client or worker with matching parameters, it automatically returns the existing connection instead of creating a new one.
Advanced Patterns
Multiple Clients Sharing One Connection
You can create multiple clients and workers - they automatically share connections when URLs match:
// All automatically share the same connection
const orderClient = await TypedAmqpClient.create({
contract: orderContract,
urls: ["amqp://localhost"], // ← Same URLs
});
const notificationClient = await TypedAmqpClient.create({
contract: notificationContract,
urls: ["amqp://localhost"], // ← Same URLs
});
const orderWorker = await TypedAmqpWorker.create({
contract: orderContract,
urls: ["amqp://localhost"], // ← Same URLs
handlers: {
/* ... */
},
});
const notificationWorker = await TypedAmqpWorker.create({
contract: notificationContract,
urls: ["amqp://localhost"], // ← Same URLs
handlers: {
/* ... */
},
});
// All automatically share one connection with 4 separate channelsMultiple Separate Connections
If you need separate connections (e.g., for different RabbitMQ clusters), just use different URLs:
// These will have separate connections
const mainClient = await TypedAmqpClient.create({
contract,
urls: ["amqp://main-cluster"], // ← Different URLs
});
const analyticsClient = await TypedAmqpClient.create({
contract,
urls: ["amqp://analytics-cluster"], // ← Different URLs
});
// Result: 2 separate connections (one per cluster)Best Practices and Limitations
Connection Configuration Best Practices
When using automatic connection sharing, follow these best practices to avoid configuration conflicts:
1. Use Consistent Connection Options
For maximum sharing benefits, use the same connectionOptions across all clients and workers, or omit them to use defaults:
// ✅ Best: Use consistent options (or omit for defaults)
const connectionOptions = { heartbeatIntervalInSeconds: 30 };
const client = await TypedAmqpClient.create({
contract,
urls: ["amqp://localhost"],
connectionOptions, // ← Same options
});
const worker = await TypedAmqpWorker.create({
contract,
urls: ["amqp://localhost"],
connectionOptions, // ← Same options = connection shared
handlers: {
/* ... */
},
});
// ✅ Also good: Omit options to use defaults
const client = await TypedAmqpClient.create({
contract,
urls: ["amqp://localhost"], // ← No options
});
const worker = await TypedAmqpWorker.create({
contract,
urls: ["amqp://localhost"], // ← No options = connection shared
handlers: {
/* ... */
},
});2. Extract Shared Configuration
For applications with multiple clients/workers, define shared configuration once:
// ✅ Recommended: Centralize connection configuration
const AMQP_CONFIG = {
urls: ["amqp://localhost"],
connectionOptions: {
heartbeatIntervalInSeconds: 30,
reconnectTimeInSeconds: 5,
},
} as const;
// All components use the same configuration
const client = await TypedAmqpClient.create({
contract: orderContract,
...AMQP_CONFIG,
});
const worker = await TypedAmqpWorker.create({
contract: orderContract,
...AMQP_CONFIG,
handlers: {
/* ... */
},
});3. Understand Configuration Conflicts
Different connectionOptions create separate connections:
// ⚠️ Warning: Different options = separate connections (may be intentional)
const client = await TypedAmqpClient.create({
contract,
urls: ["amqp://localhost"],
connectionOptions: { heartbeatIntervalInSeconds: 30 }, // ← Options A
});
const worker = await TypedAmqpWorker.create({
contract,
urls: ["amqp://localhost"],
connectionOptions: { heartbeatIntervalInSeconds: 60 }, // ← Options B (different)
handlers: {
/* ... */
},
});
// Result: 2 separate connections (different configurations)
// This may be intentional if you need different heartbeat settingsLimitations
Connection Options Must Match for Sharing
- Connections are cached by both URLs and connection options
- Different options = separate connections
- Use consistent options or omit them for automatic sharing
No Cross-Process Sharing
- Connection sharing only works within a single Node.js process
- Each process creates its own connections
- For multi-process deployments, each process will have its own connection pool
Testing Requires Cache Reset
- The singleton caches connections across tests
- In test cleanup, call
await AmqpClient._resetConnectionCacheForTesting() - See "Cleanup in tests" section below
Connection Lifecycle Tied to Usage
- Connections remain open as long as any client/worker is using them
- Connections close automatically when all references are released
- No manual connection lifecycle management available
When to Use Connection Sharing
✅ Use Connection Sharing When:
- Your application both publishes and consumes messages
- You have multiple microservices in the same process
- You want to optimize resource usage
- You're following RabbitMQ best practices
❌ Don't Use Connection Sharing When:
- You only publish OR only consume (not both)
- Clients/workers are in different processes
- You need complete isolation between components
- The added complexity isn't worth the benefits for your use case
Performance Impact
Startup Time
- Before: ~100-200ms per connection
- After: ~50ms (shared connection)
- Savings: ~50-150ms per service
Memory Usage
- Before: ~5-10 MB per connection
- After: ~5-10 MB total (shared)
- Savings: ~5-10 MB per hybrid service
Scalability
Scenario: 100 microservices, 50% are hybrid (both publish and consume)
- Before: 150 connections (100 single-purpose + 50×2 hybrid)
- After: 100 connections (100 single-purpose + 50 hybrid)
- Improvement: 33% reduction in connection count
Backward Compatibility
Connection sharing is completely backward compatible and happens automatically:
// Existing code automatically benefits from connection sharing
const client = await TypedAmqpClient.create({
contract,
urls: ["amqp://localhost"], // ← Connection automatically created
});
const worker = await TypedAmqpWorker.create({
contract,
urls: ["amqp://localhost"], // ← Same URLs = connection automatically shared
handlers: {
/* ... */
},
});
// No code changes needed - connection sharing just works!
// Result: 1 connection, 2 channels (automatically managed)Troubleshooting
Connection sharing not working
Connection sharing is automatic when URLs and connection options match. If you see multiple connections:
Check URLs match exactly:
typescript// ❌ Different URLs = different connections const client = await TypedAmqpClient.create({ contract, urls: ["amqp://localhost:5672"], }); const worker = await TypedAmqpWorker.create({ contract, urls: ["amqp://localhost"], // Different URL! handlers: { /* ... */ }, }); // ✅ Same URLs = shared connection const urls = ["amqp://localhost"]; const client = await TypedAmqpClient.create({ contract, urls, }); const worker = await TypedAmqpWorker.create({ contract, urls, // Same URL reference handlers: { /* ... */ }, });Check connection options match:
typescript// ❌ Different options = different connections const client = await TypedAmqpClient.create({ contract, urls: ["amqp://localhost"], connectionOptions: { heartbeatIntervalInSeconds: 30 }, }); const worker = await TypedAmqpWorker.create({ contract, urls: ["amqp://localhost"], connectionOptions: { heartbeatIntervalInSeconds: 60 }, // Different! handlers: { /* ... */ }, }); // ✅ Same options = shared connection const connectionOptions = { heartbeatIntervalInSeconds: 30 }; const client = await TypedAmqpClient.create({ contract, urls: ["amqp://localhost"], connectionOptions, }); const worker = await TypedAmqpWorker.create({ contract, urls: ["amqp://localhost"], connectionOptions, // Same options reference handlers: { /* ... */ }, });
Cleanup in tests
For test isolation, the internal connection cache can be reset:
import { AmqpClient } from "@amqp-contract/core";
afterEach(async () => {
await AmqpClient._resetConnectionCacheForTesting();
});