Limits & Quotas
This page documents all rate limits, payload limits, and quotas in FormaMail. We believe in transparency - knowing your limits before you hit them.
API Rate Limits
Rate limits protect the platform and ensure fair usage. Limits are applied per API key.
Email Endpoints
| Endpoint | Method | Limit | Window |
|---|---|---|---|
/api/emails/send | POST | 30 requests | per minute |
/api/emails/send/bulk | POST | 5 requests | per minute |
/api/emails | GET | 300 requests | per minute |
/api/emails/:id | GET | 300 requests | per minute |
/api/emails/:id/retry | POST | 10 requests | per minute |
Template Endpoints
| Endpoint | Method | Limit | Window |
|---|---|---|---|
/api/templates | GET | 300 requests | per minute |
/api/templates | POST | 30 requests | per minute |
/api/templates/:id | PATCH | 30 requests | per minute |
/api/templates/:id | DELETE | 30 requests | per minute |
Analytics Endpoints
| Endpoint | Method | Limit | Window |
|---|---|---|---|
/api/analytics/* | GET | 100 requests | per minute |
/api/dashboard/* | GET | 100 requests | per minute |
Webhook Subscription Endpoints
| Endpoint | Method | Limit | Window |
|---|---|---|---|
/api/v1/webhook-subscriptions | GET/POST/PATCH/DELETE | 30 requests | per minute |
/api/v1/webhook-subscriptions/:id/test | POST | 10 requests | per minute |
Rate Limit Headers
Every API response includes rate limit headers:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 25
X-RateLimit-Reset: 1732723200| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the window |
X-RateLimit-Remaining | Remaining requests in current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Rate Limit Exceeded Response
When you exceed the rate limit:
{
"statusCode": 429,
"code": "ERR_QUOTA_001",
"message": "Rate limit exceeded. Please retry after 45 seconds.",
"retryAfter": 45
}Best Practice: Implement exponential backoff when you receive a 429 response.
Payload Limits
Request Limits
| Limit | Value | Notes |
|---|---|---|
| Request body size | 10 MB | JSON payload maximum |
| URL length | 8 KB | Including query parameters |
| Header size | 16 KB | Total header size |
Email Limits
| Limit | Value | Notes |
|---|---|---|
| Recipients per email | 50 | Use bulk endpoint for more |
| Attachments per email | 10 | Files or generated |
| Total attachment size | 25 MB | Combined size |
| Single attachment | 10 MB | Per file |
| Email body (HTML) | 1 MB | Rendered template |
| Subject line | 998 characters | RFC 2822 limit |
Bulk Send Limits
| Limit | Value | Notes |
|---|---|---|
| Recipients per request | 1,000 | Batch size |
| Unique emails per batch | 1,000 | De-duplicated |
| Variables size per recipient | 100 KB | JSON per recipient |
Attachment Generation Limits
FormaMail enforces “Safe Harbor” limits on attachment generation to protect system resources and ensure reliable email delivery via AWS SES.
Why these limits? AWS SES has a 10 MB raw message limit (~7.5 MB after base64 encoding). Our limits ensure your emails always deliver successfully.
PDF Limits
| Limit | Value | Why |
|---|---|---|
| Pages per PDF | 30 | Protects CPU/compute costs |
| Images per PDF | 50 | Protects memory usage |
| Output file size | 7 MB | Guarantees AWS SES delivery |
| Page size | A4 / Letter | Standard sizes |
| Generation timeout | 60 seconds | Per document |
Excel Limits
| Limit | Value | Why |
|---|---|---|
| Rows per workbook | 10,000 | Protects memory/RAM |
| Columns per sheet | 50 | Protects file size |
| Sheets per workbook | 10 | Maximum sheets |
| Output file size | 7 MB | Guarantees AWS SES delivery |
| Cell content | 32,767 chars | Excel standard limit |
| Generation timeout | 120 seconds | Per workbook |
Template Processing Limits
| Limit | Value | Why |
|---|---|---|
| Loop iterations | 1,000 | Prevents hang-ups from large arrays |
| Nesting depth | 10 levels | Prevents infinite recursion |
| Components per template | 500 | Protects rendering performance |
Limit Violation Errors
When you exceed attachment limits, FormaMail immediately stops processing and returns a 400 Bad Request with a specific error code:
| Error Code | Limit | Example Message |
|---|---|---|
ERR_LIMIT_001 | PDF pages | ”PDF generation exceeded 30 page limit. Your PDF has 45 pages.” |
ERR_LIMIT_002 | PDF images | ”PDF contains 75 images, exceeding the 50 image limit.” |
ERR_LIMIT_003 | Excel rows | ”Excel generation exceeded 10,000 row limit. Your data has 15,000 rows.” |
ERR_LIMIT_004 | Excel sheets | ”Excel workbook exceeded 10 sheet limit. Your workbook has 12 sheets.” |
ERR_LIMIT_005 | Excel columns | ”Excel sheet exceeded 50 column limit. Your sheet has 75 columns.” |
ERR_LIMIT_006 | File size | ”Generated attachment (7.5MB) exceeds safe email limit of 7MB.” |
ERR_LIMIT_007 | Loop iterations | ”Loop “items” has 2,500 items but maximum is 1,000. Consider pagination or splitting data.” |
ERR_LIMIT_008 | Nesting depth | ”Template exceeded 10 level nesting depth. Current depth: 12 levels.” |
ERR_LIMIT_009 | Component count | ”Template exceeded 500 component limit. Your template has 650 components.” |
Example Error Response
{
"statusCode": 400,
"code": "ERR_LIMIT_001",
"message": "PDF generation exceeded maximum page limit",
"timestamp": "2025-12-09T10:30:00.000Z",
"path": "/api/attachments/generate",
"relatedInfo": {
"actualPages": 45,
"maxPages": 30,
"message": "PDF generation exceeded 30 page limit. Your PDF has 45 pages."
}
}How to Handle Limit Errors
- Split large datasets: Break data into multiple attachments
- Paginate loops: Use pagination for arrays over 1,000 items
- Optimize images: Compress images before including in PDFs
- Simplify templates: Reduce nesting and component count
Pro tip: Validate your data size before calling the API. Check array lengths and estimate row counts client-side to avoid limit errors.
Template Limits
Content Limits
| Limit | Value |
|---|---|
| Variables per template | 500 |
| Components per template | 200 |
| Nested loop depth | 3 levels |
| Conditional nesting | 5 levels |
| Formula complexity | 1,000 operations |
| Template name | 255 characters |
| Template slug | 100 characters |
Template Counts by Plan
| Plan | Email Templates | Attachment Templates |
|---|---|---|
| Free | 5 | 2 |
| Starter | 25 | 10 |
| Pro | Unlimited | Unlimited |
| Enterprise | Unlimited | Unlimited |
Account Quotas
Email Quotas by Plan
| Plan | Emails/Month | Overage |
|---|---|---|
| Free | 1,000 | Not available |
| Starter | 50,000 | $0.001/email |
| Pro | 200,000 | $0.0008/email |
| Enterprise | Custom | Custom |
Credit System
FormaMail uses a credit-based system:
- 1 email = 1 credit
- 1 PDF attachment = 1 credit
- 1 Excel attachment = 1 credit
Example: Sending an email with a PDF attachment uses 2 credits.
Credits never expire and roll over month-to-month. Buy what you need, use when ready.
Team Limits
| Plan | Team Members | Teams |
|---|---|---|
| Free | 1 | 1 |
| Starter | 5 | 1 |
| Pro | 10 | 3 |
| Enterprise | Unlimited | Unlimited |
API Key Limits
| Plan | API Keys |
|---|---|
| Free | 2 |
| Starter | 10 |
| Pro | 25 |
| Enterprise | Unlimited |
Webhook Limits
Subscription Limits
| Plan | Webhook Subscriptions |
|---|---|
| Free | 2 |
| Starter | 10 |
| Pro | 25 |
| Enterprise | Unlimited |
Delivery Limits
| Limit | Value | Notes |
|---|---|---|
| Timeout | 30 seconds | Per delivery attempt |
| Retries | 3 attempts | Exponential backoff |
| Payload size | 256 KB | Maximum webhook payload |
| Events per second | 100 | Per subscription |
OAuth Limits
Token Limits
| Limit | Value |
|---|---|
| Access token lifetime | 1 hour |
| Refresh token lifetime | 30 days |
| Authorization code lifetime | 10 minutes |
| Active access tokens per user | 50 |
| Active refresh tokens per user | 10 |
OAuth App Limits
| Plan | OAuth Apps |
|---|---|
| Free | 1 |
| Starter | 5 |
| Pro | 10 |
| Enterprise | Unlimited |
Storage Limits
Asset Storage
| Plan | Storage | File Size |
|---|---|---|
| Free | 100 MB | 5 MB/file |
| Starter | 1 GB | 10 MB/file |
| Pro | 10 GB | 25 MB/file |
| Enterprise | Custom | Custom |
Generated File Retention
| File Type | Retention |
|---|---|
| PDF attachments | 7 days |
| Excel attachments | 7 days |
| Temporary files | 24 hours |
Requesting Limit Increases
If you need higher limits, you have options:
Upgrade Your Plan
Higher plans come with increased limits. See Pricing.
Enterprise Custom Limits
Enterprise customers can request custom limits:
- Dedicated infrastructure
- Custom rate limits
- Higher storage quotas
- Dedicated IP addresses
Contact sales@formamail.com
Temporary Increases
For one-time events (product launches, campaigns), request a temporary limit increase:
Email: support@formamail.com
Include:
- Account/Team ID
- Requested limit increase
- Duration needed
- Use case description
Temporary increases are reviewed within 1 business day. Plan ahead for time-sensitive events.
Monitoring Your Usage
Dashboard
View your current usage in the dashboard:
- Overview: Current email/credit usage
- Settings → Usage: Detailed usage breakdown
- Settings → API Keys: Per-key usage statistics
API
Query usage programmatically:
GET /api/emails/usage/statsResponse:
{
"period": "2024-11",
"emailsSent": 15420,
"creditsUsed": 18500,
"creditsRemaining": 31500,
"quota": 50000
}Alerts
Set up usage alerts in Settings → Notifications:
- 50% quota usage
- 80% quota usage
- 100% quota usage
- Rate limit warnings
Best Practices
Avoid Rate Limits
- Batch operations: Use bulk endpoints instead of multiple single requests
- Cache responses: Don’t re-fetch unchanged data
- Implement backoff: Exponential backoff on 429 responses
- Use webhooks: Don’t poll for status updates
Optimize Payload Size
- Compress images: Before including in templates
- Limit data: Only include necessary variables
- Paginate queries: Use pagination for large datasets
Handle Limits Gracefully
async function sendWithRetry(payload, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch('/api/emails/send', {
method: 'POST',
headers: { 'Authorization': `Bearer ${API_KEY}` },
body: JSON.stringify(payload)
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
await sleep(retryAfter * 1000);
continue;
}
return response.json();
}
throw new Error('Max retries exceeded');
}