Performance optimization: high throughput on the Sent API

This guide shows you how to get maximum throughput from a Sent integration: work within the documented rate limits, use batching to raise your effective ceiling, stop spending requests on avoidable reads and polling, and structure sustained volume around a queue. It assumes you can already send messages.

Stay under the rate limits

All throughput planning starts from two numbers, both applied per customer account:

TierLimitWindowApplies to
Standard200 requests per minuteSliding 60-second windowAll endpoints, including POST /v3/messages
Sensitive10 requests per minuteFixed 60-second windowPOST /v3/webhooks/{id}/rotate-secret and POST /v3/webhooks/{id}/test only

Three properties of the limits shape everything else in this guide:

  • The pool is shared per account. Every API key on the account draws from the same 200 requests per minute, and requests scoped to a profile with the x-profile-id header count against the organization's pool. Budget the limit across your workers rather than letting each one assume it has 200.
  • There is no quota readout on successful responses. The X-RateLimit-* headers and Retry-After appear only on 429 responses, so treat each 429 as your pressure signal: honor Retry-After before retrying, and count 429s in your metrics.
  • Failed validation still costs budget. Requests rejected with 400 or 422 count toward the limit (only 401/403 authentication rejections do not), so an oversized or malformed batch consumes a request without sending anything.

Refer to the Rate Limits reference for the per-endpoint table and the 429 response format, and to How to handle Sent API rate limits for ready-made backoff, monitoring, and throttling implementations in TypeScript, Python, and Go.

Batch recipients to raise the ceiling

Limits count per request, not per recipient, and a single POST /v3/messages accepts up to 1,000 recipients. That makes batching the most effective optimization available: 200 requests per minute at 1,000 recipients each is a ceiling of 200,000 accepted messages per minute from one account, a thousand times what per-recipient requests would allow.

Chunk larger lists into slices of 1,000 (a request with more recipients fails validation with 400 and still counts against the limit), and give each batch a deterministic idempotency key so retries cannot double-send:

const BATCH_SIZE = 1000; // API maximum per request

for (let i = 0; i < recipients.length; i += BATCH_SIZE) {
  await client.messages.send(
    {
      to: recipients.slice(i, i + BATCH_SIZE),
      template: { id: templateId }
    },
    { idempotencyKey: `campaign_${campaignId}_batch_${i / BATCH_SIZE}` }
  );
}

If this loop runs alongside other API traffic, pace it so combined throughput stays under 200 requests per minute. The full campaign pattern, covering pacing, partial-failure handling, and reconciling accepted messages against webhook events, is in Batch Operations.

Reuse one client instance

Create the SDK client once, at startup or in your dependency container, and share it across requests and jobs. A shared client keeps HTTP connections alive and pooled, so high-volume sending does not pay a TLS handshake per request:

import SentDm from '@sentdm/sentdm';

// Create once at startup — not inside a request handler or per job
export const sent = new SentDm({ apiKey: process.env.SENT_API_KEY });

Cut requests with caching and webhooks

The cheapest request is the one you never send, and with a shared 200-per-minute pool, every read you avoid is send capacity you keep:

  • Cache repeated reads. Contact and template lookups (GET /v3/contacts/{id}, GET /v3/templates/{id}) that your send path repeats are cache candidates. Use a short TTL and remember that message and contact state changes server-side, so expire entries when webhook events tell you the underlying data moved. A worked caching example is in the rate limits guide.
  • Never poll for delivery status. Polling GET /v3/messages/{id} for every message in a campaign consumes the same pool your sends need. Subscribe to the webhook events message.delivered, message.failed, message.filtered, and message.blocked instead: delivery outcomes arrive as they happen and cost you zero requests. See Message status tracking.

Queue sends for sustained volume

For continuous high volume, as opposed to one-off campaigns, run sends through a persistent job queue (BullMQ, Sidekiq, Celery, or similar). The queue mechanics belong to your infrastructure; the Sent-specific requirements are:

  • One job per batch of up to 1,000 recipients, so each job makes exactly one POST /v3/messages call.
  • Worker concurrency capped so combined throughput stays under 200 requests per minute.
  • An idempotency key per job, so queue retries replay the original response instead of double-sending. See How to retry Sent API requests safely.
  • The message_id values from each 202 response persisted, so webhook events can be matched back to jobs.

The worked version of this pattern, including queue-depth monitoring, is in Batch Operations.

A 202 response means the batch was accepted, not delivered. Individual messages can still end FAILED, FILTERED, or BLOCKED asynchronously. Reconcile accepted IDs against webhook events rather than treating the 202 as delivery confirmation.

Verify your throughput

Your integration is performing correctly when:

  • A campaign of N recipients completes in roughly N / 1,000 requests. If your request count is near N, you are not batching.
  • Steady-state traffic produces no 429 responses, and your metrics would show a spike if it started to.
  • Delivery outcomes arrive through webhooks, and your request log shows no per-message status polling.

If you hit sustained 429s after batching, caching, and pacing, your legitimate volume exceeds the account limit. Contact support@sent.dm about an increase, as described in the Rate Limits reference.

On this page