2026-04-16·14 min read·

NIS2 Art.23 + GDPR Art.33 Dual Reporting: Simultaneous Incident Notification Developer Guide (2026)

A ransomware attack encrypts your patient records database. A cloud misconfiguration exposes 50,000 user records. An ICT outage at a payment processor leaks transaction logs. Each of these incidents triggers two separate reporting obligations under EU law — and they run in parallel from the same starting clock.

NIS2 Art.23 (for essential and important entities in critical sectors) requires notification to your national competent authority or CSIRT within 24 hours, 72 hours, and 1 month.

GDPR Art.33 (for data controllers handling personal data) requires notification to your supervisory authority (DPA) within 72 hours.

The 72-hour window is the collision point: you owe the NCA/CSIRT your intermediate NIS2 notification at the same moment you owe the DPA your GDPR breach notification. These go to different authorities, use different forms, require different content, and are enforced by different regulators with different fine regimes.

This guide provides the engineering infrastructure and practical runbook to handle both obligations simultaneously without missing either deadline.


1. When Both Obligations Apply: The Dual-Trigger Conditions

Not every NIS2 incident triggers GDPR, and not every GDPR breach triggers NIS2. Dual reporting applies when both threshold tests are satisfied simultaneously.

NIS2 Art.23 Trigger (Significant Cybersecurity Incident)

Your organisation must report under NIS2 Art.23 if:

  1. You are an essential entity (Art.3(1)) or important entity (Art.3(2)) in a NIS2-covered sector
  2. The incident is "significant" under Art.23(3) — meeting at least one of:
    • Causes or could cause severe operational disruption of your services
    • Causes or could cause financial loss to your organisation
    • Has affected or could affect other natural or legal persons with significant damage

Sectors triggering NIS2 scope: energy, transport, banking, financial market infrastructure, health, drinking water, wastewater, digital infrastructure (cloud providers, data centres, CDNs, DNS, IXPs, TLD registries), ICT service management, public administration, and space (Annex I). Also postal services, waste management, chemicals, food, manufacturing, digital providers, and research organisations (Annex II).

GDPR Art.33 Trigger (Personal Data Breach)

You must report under GDPR Art.33 if:

  1. You are a data controller under GDPR Art.4(7)
  2. A personal data breach has occurred — Art.4(12): "a breach of security leading to the accidental or unlawful destruction, loss, alteration, unauthorised disclosure of, or access to, personal data"
  3. The breach is not unlikely to result in a risk to the rights and freedoms of natural persons (low-risk breaches are exempt from DPA notification per Art.33(1), but must still be documented)

The Overlap Zone

Dual reporting is mandatory when:

In practice, this means: any cloud provider, hospital, bank, energy company, or critical service operator that processes personal data and suffers a meaningful incident faces dual reporting. For most NIS2-covered entities processing user data, the dual-trigger zone is the default scenario — not the edge case.


2. Authority Routing: Who Receives Which Report

The first implementation challenge is routing. The two reports go to completely different authorities with different mandates, contact channels, and jurisdictions.

DimensionNIS2 Art.23GDPR Art.33
RecipientNational Competent Authority (NCA) / CSIRTSupervisory Authority (DPA)
Examples (DE)BSI (Federal Office for Information Security)Landesbeauftragter (state DPA by sector)
Examples (FR)ANSSICNIL
Examples (NL)NCSC-NLAutoriteit Persoonsgegevens
Examples (IE)NCSC IrelandData Protection Commission
MandateCybersecurity resilience and sector-specific protectionData subject rights and personal data protection
FormNIS2-specific incident notification portal (varies by member state)DPA-specific breach notification portal or web form
LanguageMember-state language required for final reportsMember-state language for DPA portal
SanctionsNIS2 Art.32/33: up to €10M or 2% turnover (EE); €7M or 1.4% (IE)GDPR Art.83: up to €20M or 4% global turnover

Critical practical point: In Germany, the BSI and the relevant Landesbeauftragter are different organisations in different cities, with different online portals, different email addresses, and different follow-up processes. Your incident response runbook must have two separate authority contacts pre-loaded before an incident occurs.

Multi-Member-State Complexity

NIS2 Art.23 introduces cross-border notification obligations when your services operate in multiple EU member states. You may need to notify the NCAs of every member state where your services are disrupted — not just your home member state.

GDPR similarly requires notifying the lead supervisory authority for cross-border processing under Art.55-57, but the one-stop-shop mechanism (Art.56) means you usually notify only your lead DPA (determined by your main establishment).

Result: a cross-border incident may trigger NIS2 notifications to 3-5 NCAs plus a single lead DPA GDPR notification. Track which member states are affected and their NCA contact details.


3. Timeline Analysis: The 72-Hour Collision

Both frameworks share the 72-hour window — but they diverge before and after it.

Hour 0 — Incident Awareness Timestamp (NIS2 + GDPR clock starts)

                    NIS2 Art.23                    GDPR Art.33
                    -----------                    -----------
Hour 0-24:    Early Warning → NCA/CSIRT           (no action required)
                    [known/suspected incident,
                     preliminary classification]

Hour 24-72:   Intermediate Notification → NCA     Breach Notification → DPA
                    [initial assessment,           [nature of breach,
                     indicators of compromise,      categories + approx.
                     affected systems/services]     number of records/
                                                    subjects, likely
                                                    consequences, measures
                                                    taken/proposed]

Hour 72+:           (ongoing monitoring)          (complete if 72h missed
                                                   with explanation)

Day 30:       Final Report → NCA                   (DPA may request update)
                    [root cause, measures,
                     cross-border impact,
                     quantitative assessment]

The 72-Hour Pressure Point

At exactly 72 hours, your team must simultaneously:

  1. File the NIS2 intermediate notification to the NCA/CSIRT
  2. File the GDPR Art.33 breach notification to the DPA

Both use the same awareness timestamp as their starting clock. Both have parallel deadlines. Both require different content and go to different portals.

Under real incident conditions — partial information, ongoing forensics, potential CLOUD Act complications on your cloud provider's logs — this 72-hour window is where organisations fail compliance.

What "Awareness" Means in Each Framework

NIS2 Art.23: The clock starts when your organisation becomes aware that a significant incident "has occurred or is occurring." This includes awareness of the possibility of a significant incident before it is confirmed.

GDPR Art.33(1): The clock starts when the data controller "becomes aware of" the personal data breach. EDPB Guidelines 01/2021 clarify: awareness means when the controller has "a reasonable degree of certainty that a security incident has occurred that has led to personal data being compromised."

Key difference: NIS2 awareness may precede GDPR awareness if the cybersecurity incident is detected first but personal data involvement is confirmed later. In this case:

Document both timestamps separately. The clocks run independently. A late-stage GDPR discovery does not extend the NIS2 deadline.


4. Content Divergence: What Each Report Must Say

Filing two reports with identical content will satisfy neither regulator. The authorities expect reports tailored to their framework's specific requirements.

NIS2 Art.23 Content Requirements

Stage 1 — Early Warning (≤24h):

Stage 2 — Intermediate Notification (≤72h):

Stage 3 — Final Report (≤1 month):

GDPR Art.33 Content Requirements

A single breach notification to the DPA must include (Art.33(3)):

GDPR does not require you to know the root cause at the 72-hour mark — you can file an incomplete notification with explanation and supplement it later (Art.33(4)). However, you must provide what you do know, and "we are still investigating" is not sufficient for the nature of breach field.

The Content Overlap and the Content Gap

What you can reuse: The factual description of the incident, the systems affected, the timeline of detection, and the initial mitigation measures are common to both reports. You can draft a single incident facts document and extract from it.

What you cannot reuse:

A ransomware incident that disrupted your hospital's EHR system for 6 hours and exposed 12,000 patient records requires:


5. Python DualIncidentReporter Implementation

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
import json
import hashlib

class IncidentSeverity(Enum):
    SIGNIFICANT_NIS2 = "significant_nis2"
    PERSONAL_DATA_BREACH_GDPR = "personal_data_breach_gdpr"
    DUAL_TRIGGER = "dual_trigger"

class EntityType(Enum):
    ESSENTIAL_ENTITY = "essential_entity"      # NIS2 Annex I
    IMPORTANT_ENTITY = "important_entity"       # NIS2 Annex II
    CONTROLLER_ONLY = "controller_only"         # GDPR only, no NIS2 scope

@dataclass
class PersonalDataContext:
    """GDPR Art.33 specific context"""
    approximate_subjects: int
    categories: list[str]           # e.g., ["health", "financial", "location"]
    special_category: bool          # Art.9 data: health, biometric, etc.
    approximate_records: int
    likely_consequences: str
    dpo_contact: str
    mitigation_measures: str
    dpd_supplementary_possible: bool = True  # can supplement if incomplete

@dataclass
class NIS2IncidentContext:
    """NIS2 Art.23 specific context"""
    is_suspected_malicious: bool
    cross_border_potential: bool
    affected_member_states: list[str]
    affected_services: list[str]
    service_disruption_hours: float
    indicators_of_compromise: list[str]
    root_cause: Optional[str] = None  # required in Stage 3 only

@dataclass
class DualIncidentReport:
    """Manages simultaneous NIS2 Art.23 + GDPR Art.33 reporting"""

    # Shared fields
    incident_id: str
    awareness_timestamp: datetime   # NIS2 clock start
    gdpr_awareness_timestamp: Optional[datetime] = None  # GDPR clock start (may differ)
    organisation_name: str = ""
    organisation_lei: str = ""      # Legal Entity Identifier for DORA-scope entities
    main_establishment_ms: str = "" # EU member state for GDPR lead DPA

    # Framework-specific context
    nis2_context: Optional[NIS2IncidentContext] = None
    gdpr_context: Optional[PersonalDataContext] = None

    # Entity classification
    entity_type: EntityType = EntityType.ESSENTIAL_ENTITY

    def gdpr_clock_start(self) -> datetime:
        """Returns the GDPR 72h clock start — may differ from NIS2"""
        return self.gdpr_awareness_timestamp or self.awareness_timestamp

    def nis2_deadlines(self) -> dict[str, datetime]:
        t0 = self.awareness_timestamp
        return {
            "stage_1_early_warning": t0 + timedelta(hours=24),
            "stage_2_intermediate": t0 + timedelta(hours=72),
            "stage_3_final_report": t0 + timedelta(days=30),
        }

    def gdpr_deadlines(self) -> dict[str, datetime]:
        t0 = self.gdpr_clock_start()
        return {
            "art33_dpa_notification": t0 + timedelta(hours=72),
        }

    def collision_window(self) -> dict:
        """Identifies the 72h dual-filing collision window"""
        nis2_72 = self.nis2_deadlines()["stage_2_intermediate"]
        gdpr_72 = self.gdpr_deadlines()["art33_dpa_notification"]
        hours_gap = abs((nis2_72 - gdpr_72).total_seconds() / 3600)
        return {
            "nis2_intermediate_deadline": nis2_72.isoformat(),
            "gdpr_dpa_deadline": gdpr_72.isoformat(),
            "hours_gap": hours_gap,
            "simultaneous_filing_required": hours_gap < 2.0,
            "note": "Both reports may be due within 2 hours of each other if awareness timestamps align",
        }

    def authority_routing(self) -> dict:
        """Returns the required authority contacts for this incident"""
        routing = {
            "nis2_primary_nca": self._get_nca(self.main_establishment_ms),
            "nis2_cross_border_ncas": [
                self._get_nca(ms)
                for ms in (self.nis2_context.affected_member_states if self.nis2_context else [])
                if ms != self.main_establishment_ms
            ],
            "gdpr_lead_dpa": self._get_dpa(self.main_establishment_ms),
        }
        return routing

    def _get_nca(self, member_state: str) -> str:
        nca_map = {
            "DE": "BSI (Bundesamt für Sicherheit in der Informationstechnik) — bsi.bund.de",
            "FR": "ANSSI (Agence nationale de la sécurité des systèmes d'information) — anssi.fr",
            "NL": "NCSC-NL (Nationaal Cyber Security Centrum) — ncsc.nl",
            "IE": "NCSC Ireland — ncsc.gov.ie",
            "AT": "CERT.at / GovCERT Austria — cert.at",
            "PL": "CERT Polska / CSIRT GOV — cert.pl",
            "ES": "INCIBE-CERT / CCN-CERT — incibe.es",
            "IT": "ACN / CSIRT Italia — acn.gov.it",
        }
        return nca_map.get(member_state.upper(), f"NCA for {member_state} — consult ENISA NIS authorities map")

    def _get_dpa(self, member_state: str) -> str:
        dpa_map = {
            "DE": "Relevant Landesbeauftragter (state) + BfDI for federal — bfdi.bund.de",
            "FR": "CNIL — cnil.fr",
            "NL": "Autoriteit Persoonsgegevens — autoriteitpersoonsgegevens.nl",
            "IE": "Data Protection Commission — dataprotection.ie",
            "AT": "Datenschutzbehörde — dsb.gv.at",
            "PL": "Urząd Ochrony Danych Osobowych (UODO) — uodo.gov.pl",
            "ES": "Agencia Española de Protección de Datos (AEPD) — aepd.es",
            "IT": "Garante per la protezione dei dati personali — garanteprivacy.it",
        }
        return dpa_map.get(member_state.upper(), f"Lead DPA for {member_state} — consult EDPB DPA directory")

    def nis2_stage1_report(self) -> dict:
        if not self.nis2_context:
            return {"error": "No NIS2 context provided"}
        return {
            "report_type": "NIS2_Art23_Stage1_EarlyWarning",
            "incident_id": self.incident_id,
            "awareness_timestamp": self.awareness_timestamp.isoformat(),
            "deadline": self.nis2_deadlines()["stage_1_early_warning"].isoformat(),
            "suspected_malicious": self.nis2_context.is_suspected_malicious,
            "cross_border_potential": self.nis2_context.cross_border_potential,
            "preliminary_info": "Incident detected — full assessment in progress",
            "authority": self._get_nca(self.main_establishment_ms),
        }

    def nis2_stage2_report(self) -> dict:
        if not self.nis2_context:
            return {"error": "No NIS2 context provided"}
        return {
            "report_type": "NIS2_Art23_Stage2_IntermediateNotification",
            "incident_id": self.incident_id,
            "deadline": self.nis2_deadlines()["stage_2_intermediate"].isoformat(),
            "nature_of_incident": "Cybersecurity incident — type under investigation",
            "affected_services": self.nis2_context.affected_services,
            "indicators_of_compromise": self.nis2_context.indicators_of_compromise,
            "service_disruption_hours": self.nis2_context.service_disruption_hours,
            "mitigation_applied": "Isolation and forensic investigation initiated",
            "authority": self._get_nca(self.main_establishment_ms),
            "cross_border_ncas": [
                self._get_nca(ms) for ms in self.nis2_context.affected_member_states
            ],
        }

    def gdpr_art33_notification(self) -> dict:
        if not self.gdpr_context:
            return {"error": "No GDPR context provided"}
        ctx = self.gdpr_context
        return {
            "report_type": "GDPR_Art33_BreachNotification",
            "incident_id": self.incident_id,
            "gdpr_awareness_timestamp": self.gdpr_clock_start().isoformat(),
            "deadline": self.gdpr_deadlines()["art33_dpa_notification"].isoformat(),
            "breach_nature": {
                "categories_of_data": ctx.categories,
                "approximate_subjects": ctx.approximate_subjects,
                "approximate_records": ctx.approximate_records,
                "special_category_data": ctx.special_category,
                "art9_category": "health/biometric/genetic" if ctx.special_category else "N/A",
            },
            "dpo_contact": ctx.dpo_contact,
            "likely_consequences": ctx.likely_consequences,
            "measures_taken": ctx.mitigation_measures,
            "supplementary_possible": ctx.dpd_supplementary_possible,
            "authority": self._get_dpa(self.main_establishment_ms),
            "note": "Art.33(4): Incomplete notification permissible — supplement with explanation",
        }

    def dual_filing_runbook(self) -> str:
        collision = self.collision_window()
        nis2_dl = self.nis2_deadlines()
        gdpr_dl = self.gdpr_deadlines()

        return f"""
=== DUAL INCIDENT REPORTING RUNBOOK ===
Incident ID: {self.incident_id}
Organisation: {self.organisation_name}
NIS2 Clock Start: {self.awareness_timestamp.isoformat()}
GDPR Clock Start: {self.gdpr_clock_start().isoformat()}

--- DEADLINE SCHEDULE ---
T+24h  [{nis2_dl['stage_1_early_warning'].strftime('%Y-%m-%d %H:%M')}]  NIS2 Stage 1 → {self._get_nca(self.main_establishment_ms)[:30]}
T+72h  [{nis2_dl['stage_2_intermediate'].strftime('%Y-%m-%d %H:%M')}]  NIS2 Stage 2 → NCA (simultaneous filing window)
T+72h  [{gdpr_dl['art33_dpa_notification'].strftime('%Y-%m-%d %H:%M')}]  GDPR Art.33  → {self._get_dpa(self.main_establishment_ms)[:30]}
T+30d  [{nis2_dl['stage_3_final_report'].strftime('%Y-%m-%d %H:%M')}]  NIS2 Stage 3 → NCA final report

--- 72h COLLISION WINDOW ---
NIS2 Intermediate: {collision['nis2_intermediate_deadline']}
GDPR DPA:          {collision['gdpr_dpa_deadline']}
Gap (hours):       {collision['hours_gap']:.1f}
Simultaneous:      {collision['simultaneous_filing_required']}

--- ACTION: File both reports within the same work session at T+72h ---
"""

# Example usage
if __name__ == "__main__":
    incident = DualIncidentReport(
        incident_id=hashlib.sha256(b"hospital-ehr-breach-2026").hexdigest()[:16],
        awareness_timestamp=datetime(2026, 6, 10, 8, 0, 0),
        organisation_name="EU Regional Hospital",
        main_establishment_ms="DE",
        entity_type=EntityType.ESSENTIAL_ENTITY,
        nis2_context=NIS2IncidentContext(
            is_suspected_malicious=True,
            cross_border_potential=False,
            affected_member_states=["DE"],
            affected_services=["Electronic Health Records", "Patient Portal"],
            service_disruption_hours=6.5,
            indicators_of_compromise=["C2 IP: 185.x.x.x", "Malware hash: abc123"],
            root_cause=None,  # Not known yet at 72h
        ),
        gdpr_context=PersonalDataContext(
            approximate_subjects=12000,
            categories=["health", "identity"],
            special_category=True,
            approximate_records=85000,
            likely_consequences="Exposure of medical history, potential insurance discrimination risk",
            dpo_contact="dpo@hospital.de / +49-xxx-xxx",
            mitigation_measures="Affected systems isolated, passwords reset, forensic investigation initiated",
        ),
    )

    print(incident.dual_filing_runbook())
    print(json.dumps(incident.nis2_stage2_report(), indent=2))
    print(json.dumps(incident.gdpr_art33_notification(), indent=2))

6. Cloud Provider Forensics and the CLOUD Act Complication

During dual reporting, both the NCA and the DPA will ask for forensic evidence: access logs, authentication records, data exfiltration indicators. If your infrastructure runs on US cloud providers (AWS, Azure, GCP, Cloudflare), this creates a jurisdictional tension.

18 U.S.C. § 2713 (CLOUD Act) authorises US law enforcement to compel US-incorporated cloud providers to produce data stored anywhere in the world, including data in EU-region data centres, without following GDPR Art.48 mutual legal assistance procedures. Your forensic evidence — the very logs you need to satisfy both NCA and DPA reporting — may be subject to US law enforcement access without your knowledge during an active EU incident investigation.

GDPR Art.48 prohibits transfers of personal data to non-EU authorities except through mutual legal assistance channels (MLATs). If a US cloud provider responds to a US law enforcement CLOUD Act order during your incident and transfers EU personal data from your breach investigation to the US, this creates a secondary Art.48 exposure on top of the original breach.

Practical implications for dual reporting:


7. Special Scenarios

Scenario A: NIS2 Incident Without Personal Data

A DDoS attack disrupts your DNS service for 4 hours but affects no personal data. NIS2 Art.23 applies. GDPR Art.33 does not. File only the NIS2 three-stage report.

Scenario B: Personal Data Breach Without NIS2 Scope

A SaaS startup (not NIS2-covered) suffers an SQL injection exposing 5,000 user records. GDPR Art.33 applies. NIS2 does not (company is not in an Annex I/II sector or does not meet size thresholds). File only the GDPR breach notification.

Scenario C: DORA + NIS2 + GDPR Triple Reporting

A financial entity (bank, payment processor, insurance company) under DORA Regulation 2022/2554 suffers a major ICT incident involving personal data. Three simultaneous obligations:

DORA Art.19(6) provides a partial simplification: the competent authority receiving the DORA Art.19 report shall transmit it to the NIS2 competent authority. This means a single DORA report satisfies the NIS2 Stage 2 notification obligation for financial entities — but the GDPR Art.33 DPA notification remains independent.

Scenario D: Late GDPR Awareness

Incident detected at T=0 as a server breach. Personal data involvement confirmed at T=36h during forensic investigation.


8. The 25-Item Dual Reporting Checklist

Pre-Incident Preparation (do this now, not at 2am during a breach)

At T=0 (Incident Detected)

At T≤24h (NIS2 Stage 1)

At T≤72h (Dual Filing Window)

At T≤1 Month (NIS2 Stage 3)


9. Why Infrastructure Jurisdiction Matters for Dual Reporting

Choosing EU-native infrastructure eliminates the CLOUD Act forensic complication described in Section 6. For NIS2-covered entities and GDPR controllers, this means:

sota.io is an EU-incorporated PaaS (no US parent entity, no CLOUD Act scope) with infrastructure in Germany and Netherlands. For organisations subject to NIS2 and GDPR dual-reporting obligations, forensic data sovereignty is a direct compliance requirement — not just a preference.

Deploy your NIS2+GDPR-dual-covered application in minutes:

# Install sota CLI
npm install -g @sota-io/cli

# Deploy with EU-jurisdiction infrastructure
sota deploy --region eu-west
# → Deployed to EU jurisdiction
# → No CLOUD Act exposure
# → NIS2 + GDPR forensic data sovereignty confirmed

Summary

When a security incident involves personal data on NIS2-covered infrastructure, EU organisations face simultaneous reporting obligations to two different authorities on the same 72-hour clock:

The content requirements differ fundamentally: NIS2 focuses on operational impact and cybersecurity indicators; GDPR focuses on data subject risk and personal data categories. Both reports must be tailored to their respective frameworks using the same underlying incident facts.

For financial entities, DORA Art.19 adds a 4-hour third obligation, though DORA Art.19(6) provides a partial NIS2 cross-notification mechanism.

Engineering for dual-reporting readiness requires pre-registered authority contacts, separate templates for NIS2 and GDPR content, and a clear understanding of which clock starts when — including the possibility that the GDPR clock starts later than the NIS2 clock.

The Python DualIncidentReporter class above provides a production-ready starting point for building this capability into your incident response infrastructure before the breach occurs.


See Also