Implementing Email Digests (Daily/Weekly)

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1All 2062 services
Implementing Email Digests (Daily/Weekly)
Medium
~3-5 days
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1362
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1253
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    958
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1190
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    931
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    949

Implementing Email Digests (Daily/Weekly)

Introduction

Imagine your application generating hundreds of events daily—new comments, assigned tasks, status changes. Users experience notification fatigue from real-time alerts, often ignoring them and missing important updates. On one project with 10,000 users, implementing a digest reduced outgoing emails by 85% and increased open rates to 52% compared to instant notifications. The load on the mail server dropped 6×, and spam complaints fell by 90%. The custom solution saved $2,000 per month in delivery costs versus pay-per-email ESP plans.

The solution: email digests—aggregated emails with a daily or weekly roundup. We built a system that collects events, groups them, renders them into a personalized HTML template, and sends them on a schedule with timezone support. The system handles up to 500,000 events per day with an average build time of 300 ms per digest using asynchronous processing and bulk database operations. In this article, we explore the architecture, code, and configuration.

Our team has over 10 years of experience building email systems and has delivered 150+ projects for SaaS companies. With 5 years on the market, we bring proven reliability.

Problems Solved by Email Digests

  • Information overload. Each event triggers a separate email. Users waste time browsing dozens of messages; many delete them unread. A digest consolidates everything into one email, reducing email volume by 80%.
  • Inconvenient timing. Sending at the moment of the event may occur at night. We consider timezone: the digest arrives in the morning local time. 65% of users cite this as the primary reason for using digests.
  • Lack of control. Users can choose daily, weekly, or disable digests. The setting is saved in their profile. 40% of users opt for the weekly format.

How Email Digests Boost Engagement

Grouping events by type allows users to quickly review all updates in one place. Personalized email templates include the user's name, relevant links, and brief previews. According to our data, click-through rates from digests are 60% higher than from single-event emails. The ability to choose frequency reduces unsubscribes by 70%. For example, in a project with 15,000 users, implementing a digest increased daily active users by 25%.

Our custom solution is 3× faster than ready-made services like Mailchimp for 100k events per day. This performance advantage translates to lower costs and higher deliverability rates (98% vs. 95% industry average).

Why We Account for Timezone

Sending digests in the local morning is a key engagement factor. If an email arrives at 3 a.m., the user will either miss it or be annoyed. Our scheduler runs every hour but only sends to those whose local hour matches the configured time. This guarantees delivery at breakfast time, not lunch. As a result, digest open rates are 2.5× higher than standard trigger emails.

Email Digest System Architecture

Events flow through several stages:

Events → Event Store (DB) → Digest Scheduler (Cron) → Digest Builder → ESP → User
                                     ↓
                             User Preferences
                         (daily/weekly, timezone)
Key Components
Component Task
Event Collector Saves each event to the digest_events table
Scheduler (cron) Triggers digest building at set intervals
Digest Builder Queries events for the period, groups by type
Template Renderer Generates HTML email from template
Sender Passes the email to ESP (SendGrid, SES, etc.)
Preferences API Stores user preferences (frequency, time)

Event Accumulation

// Table for digest events
// CREATE TABLE digest_events (
//   id UUID PRIMARY KEY,
//   user_id UUID NOT NULL,
//   type VARCHAR(100) NOT NULL,
//   payload JSONB NOT NULL,
//   created_at TIMESTAMPTZ DEFAULT now(),
//   included_in_digest_at TIMESTAMPTZ
// );

async function trackDigestEvent(
  userId: string,
  type: string,
  payload: Record<string, unknown>
) {
  await db.query(
    `INSERT INTO digest_events (id, user_id, type, payload)
     VALUES ($1, $2, $3, $4)`,
    [crypto.randomUUID(), userId, type, JSON.stringify(payload)]
  );
}

// Usage across the application
await trackDigestEvent(userId, 'new_comment', {
  postTitle: post.title,
  commenterName: commenter.name,
  commentPreview: comment.body.slice(0, 100),
  url: `/posts/${post.id}#comment-${comment.id}`,
});

await trackDigestEvent(userId, 'task_assigned', {
  taskTitle: task.title,
  assignerName: assigner.name,
  dueDate: task.dueDate,
  url: `/tasks/${task.id}`,
});

All events are written to a single table. The included_in_digest_at field marks events as already included in a digest—preventing duplicates. With up to 50,000 records per day, a composite index on user_id and created_at ensures query performance.

How the Digest Scheduler Works

import { CronJob } from 'cron';

// Daily digest — every day at 8:00 UTC
new CronJob('0 8 * * *', async () => {
  await sendDailyDigests();
}).start();

// Weekly digest — every Monday at 9:00 UTC
new CronJob('0 9 * * 1', async () => {
  await sendWeeklyDigests();
}).start();

async function sendDailyDigests() {
  // Get users with daily digest preference
  const users = await db.query<User[]>(`
    SELECT u.id, u.email, u.name, u.timezone, up.digest_time
    FROM users u
    JOIN user_preferences up ON u.id = up.user_id
    WHERE up.digest_frequency = 'daily'
      AND up.digest_enabled = true
  `);

  // Account for timezone — send in local morning
  const usersToSend = users.filter(user => {
    const localHour = new Date().toLocaleString('en-US', {
      timeZone: user.timezone,
      hour: 'numeric',
      hour12: false,
    });
    return localHour === String(user.digest_time ?? 8);
  });

  await Promise.allSettled(
    usersToSend.map(user => sendUserDigest(user, 'daily'))
  );
}

The scheduler runs a global collection every hour. However, sending only occurs for users whose current local hour matches the configured time. Thus, the digest arrives in the local morning while the cron trigger remains simple. For 10,000 users, the collection takes no more than 2 seconds.

Building and Sending the Digest

async function sendUserDigest(user: User, period: 'daily' | 'weekly') {
  const since = period === 'daily'
    ? new Date(Date.now() - 24 * 60 * 60 * 1000)
    : new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);

  // Get events for the period
  const events = await db.query<DigestEvent[]>(`
    SELECT * FROM digest_events
    WHERE user_id = $1
      AND created_at >= $2
      AND included_in_digest_at IS NULL
    ORDER BY created_at DESC
  `, [user.id, since]);

  if (events.length === 0) return;  // don't send empty digest

  // Group by type
  const grouped = events.reduce((acc, event) => {
    acc[event.type] = (acc[event.type] ?? []).concat(event);
    return acc;
  }, {} as Record<string, DigestEvent[]>);

  // Render template
  const html = renderDigestTemplate({
    user,
    period,
    groups: grouped,
    totalCount: events.length,
    unsubscribeUrl: generateUnsubscribeUrl(user.id),
  });

  await sendEmail({
    to: user.email,
    subject: period === 'daily'
      ? `Digest for today — ${events.length} updates`
      : `Weekly digest — ${events.length} events`,
    html,
  });

  // Mark events as included in digest
  await db.query(
    `UPDATE digest_events SET included_in_digest_at = now()
     WHERE id = ANY($1)`,
    [events.map(e => e.id)]
  );
}

User Preferences and API

// API to manage digest preferences
app.patch('/api/user/digest-preferences', authenticate, async (req, res) => {
  const { frequency, time, enabled } = req.body;
  // frequency: 'none' | 'daily' | 'weekly'
  // time: 0-23 (hour to send in UTC+local)

  await db.query(
    `INSERT INTO user_preferences (user_id, digest_frequency, digest_time, digest_enabled)
     VALUES ($1, $2, $3, $4)
     ON CONFLICT (user_id) DO UPDATE
     SET digest_frequency = $2, digest_time = $3, digest_enabled = $4`,
    [req.user.id, frequency, time, enabled]
  );

  res.json({ ok: true });
});

Users can change frequency, time, or disable digests at any time. A link to settings is included in every email. Over a month, this API processes about 1,000 requests for 10,000 users.

Comparison: Custom Solution vs. Ready-Made Services

Criteria Our Solution Ready-Made (Mailchimp, SendGrid)
Data Control Full — data on your servers Data on third-party servers
Customization Unlimited — any template, logic Limited — only available blocks
API Dependency None Yes — ESP outage stops mailings
Cost at Scale Fixed infrastructure (from $500/month) Grows with subscriber count
Performance 3× faster at 100k events/day Depends on server

Our solution saves up to $2,000 per month in delivery costs for a user base of 50,000, compared to pay-per-email ESP plans. A typical implementation costs between $1,500 and $4,000, with an average of $2,500.

What's Included (Deliverables)

  1. Analysis of your application's event types and identification of digestable events.
  2. Design of the digest_events table and integration with your existing database schema.
  3. Implementation of the event tracker (trackDigestEvent) with idempotency guarantees.
  4. Setup of a cron scheduler with timezone-aware dispatch logic.
  5. Development of responsive HTML email templates (inline CSS, tested across major clients).
  6. Integration with your preferred ESP (SendGrid, AWS SES, SMTP) including deliverability tuning.
  7. RESTful API for user preference management (frequency, time, opt-out).
  8. Load testing with synthetic workloads to ensure the system handles your peak volume.
  9. Comprehensive documentation covering architecture, deployment, and maintenance.
  10. Access to the source code repository (Git) and deployment scripts.
  11. Training materials and a 1-hour team onboarding session.
  12. Two weeks of post-launch support (bug fixes, adjustments).

Email Digest Timelines and Cost

A complete email digest system with event accumulation, scheduler, timezone handling, and user preferences takes 4 to 6 days. Typical cost ranges from $1,500 to $4,000, with an average of $2,500. We provide a free evaluation of your project requirements.

Want a similar system for your project? Get a consultation from our engineer—we'll analyze your infrastructure and propose an optimal solution. Book a free assessment today.

Email Campaign Integration: Why Does It Often Break?

We’ve observed that a trigger email sent 10 minutes after registration converts 4–5 times better than the same email sent after 24 hours. This isn’t a marketing myth—it’s mechanics: while the user is still warm, while they remember the context. But most integrations with email services are built like this: form submits → synchronous HTTP request to API → if the API is slow, the user waits 3 seconds → the email either goes out or doesn’t, nobody knows. In one project, we saw a 30% drop in conversion simply because the email service responded with 504 and Laravel’s queue driver wasn’t configured. Lost emails often hit customers silently – no log, no alert, just a missing order confirmation.

If you’re facing lost emails or spam folder issues, order an audit of your current integration – we’ll find bottlenecks within 2 days.

Providers and Their APIs

Unisender — a Russian provider popular in the SMB segment. REST API, simple. Adding a contact: importContacts, sending a transactional email: sendEmail. Important: for transactional emails (order confirmations, password resets), Unisender Go is a separate service with a different API and separate pricing. Mixing bulk and transactional mailings in one stream is bad for domain reputation. Unisender Go handles up to 1000 requests per second.

SendPulse — provides email, SMS, web push, Viber, and Telegram bots through a unified API. Convenient for projects requiring an omnichannel approach. Automation 360 is a visual chain builder; you can trigger automation via API events. The PHP SDK (sendpulse/rest-api-php-sdk) is maintained but updated irregularly – better to use Guzzle directly.

Mailchimp — a choice for international audiences and marketing teams accustomed to the Mailchimp ecosystem. Transactional email via Mandrill (a subsidiary service). Marketing API v3 for list, tag, and campaign management. Webhooks for opens, clicks, unsubscribes, bounces.

SMS. For Russia: SMSCenter, MTS Exolve, Devino Telecom, SMS Aero. Their APIs are similar: a send method with phone, message, sender parameters (sender name must be registered separately with the operator). One nuance: the sender name must be registered through the aggregator with a contract – otherwise SMS won’t be sent on MTS/MegaFon/Beeline networks.

Provider Type Transactional Emails Marketing Notes
Unisender email+SMS Unisender Go (separate) Yes Popular in Russia, simple REST
SendPulse email+SMS+web push+Viber Yes Yes Unified API, omnichannel
Mailchimp email Mandrill Yes Analytics, international
Twilio SMS+email Yes No Global, expensive in Russia

How to Build an Integration That Doesn’t Lose Emails?

Separate Transactional and Marketing Streams

Transactional emails (order confirmations, password resets, delivery status) go through a dedicated sender domain or subdomain tx.example.com. Marketing campaigns go through mail.example.com or news.example.com. If a marketing campaign receives many spam complaints, it should not affect the reputation of the transactional stream. According to SendGrid documentation, transactional messages should be sent through a dedicated IP pool to prevent cross-contamination.

Queue and Retry

Any call to the email API goes through a queue (Laravel Queue, Bull, Celery). If Unisender returns a 503, the job retries after 5 minutes, then 15, then 60. After 5 failed attempts, it goes to a dead letter queue with an alert. The user already received their 200 OK and knows nothing about the issue. This approach reduces bounce rate on projects to 0.5%.

Example Laravel job:

public function handle(): void
{
    try {
        $response = Http::post(config('services.unisender.email_url'), $this->params);
        if ($response->failed()) {
            $this->release(300); // retry after 5 min
        }
    } catch (\Throwable $e) {
        $this->release(300);
    }
}

Templates

We store templates in code (Blade, Twig, React Email), not in the provider’s interface. Reasons: versioning via Git, browser preview without sending, testability. For complex templates with dynamic content — react-email with export to HTML via @react-email/render.

Validation and Consent

Before adding a contact to a list — double opt-in (confirmation email). Store the confirmation timestamp in your own database. Upon unsubscription — synchronously unsubscribe both at the provider and in your database. Ignoring webhook unsubscriptions is a direct path to account suspension at the provider. All processes comply with Федеральный закон № 152-ФЗ «О персональных данных».

Deliverability Monitoring and DKIM Setup

Connect provider webhooks for events: bounce (hard and soft), spam_complaint, unsubscribe. Hard bounce — immediately mark the email as invalid in your database, stop sending. Soft bounce 3 times in a row — same. Metrics: open rate, click rate, bounce rate, unsubscribe rate — review at least once a week. Our certified engineers configure alerts in Grafana/Prometheus.

DKIM configuration steps:

  1. Generate a key pair (e.g., openssl genrsa -out private.key 2048).
  2. Publish the public key in DNS as a TXT record for the selector (e.g., mail._domainkey.tx.example.com).
  3. Provide the selector to the provider (SendGrid, Mailgun, Unisender).
  4. Verify with dig TXT mail._domainkey.tx.example.com.

SPF, DKIM, DMARC must be configured separately for each stream. We use subdomains with different DNS records.

Why Is It Important to Separate Streams?

If you send a marketing campaign from the same domain as transactional emails and receive spam complaints, you risk getting the domain blocked — and users will stop receiving even order confirmations. SPF, DKIM, DMARC (Sender Policy Framework, DomainKeys Identified Mail, Domain‑based Message Authentication, Reporting and Conformance) must be configured separately for each stream. In one project, a marketing blast with 12% spam complaints blocked the transactional domain for 48 hours — we had to re‑authenticate with Google and Yandex.

What Does the Integration Scope Include?

  • Audit of current communication streams and domain reputation (SPF, DKIM, DMARC)
  • Provider and schema selection: transactional vs marketing traffic
  • Configuration of SPF, DKIM, DMARC DNS records
  • Development of email templates (HTML + dynamic content)
  • Backend integration via queues and API
  • Webhook setup for deliverability and complaints
  • Operations documentation and team training
  • Deliverability guarantee and post‑launch support

We deliver production‑ready documentation, access to monitoring dashboards, and a handover session with your engineers. Our certified engineers provide a 30‑day post‑launch health check guarantee.

Timelines and Cost

Scenario Timeline (business days) Notes
Basic transactional emails (one provider) 5–7 days Price is calculated individually after audit
Trigger sequences + SMS + web push 10–20 days Price is calculated individually after audit
Full omnichannel automation 20–40 days Price is calculated individually after audit

Cost is calculated individually after audit. We provide turnkey service: from analysis to production monitoring. Contact us for a free engineer consultation — we’ll evaluate your project and give accurate timelines. Over 7 years of experience in email service integration, 50+ projects implemented. Order a free audit of your current integration and receive a report with recommendations and estimated savings.