{"id":550,"date":"2026-11-11T01:26:46","date_gmt":"2026-11-11T06:56:46","guid":{"rendered":"https:\/\/photonconsole.com\/blog\/?p=550"},"modified":"2026-11-11T10:17:50","modified_gmt":"2026-11-11T15:47:50","slug":"migrating-from-amazon-ses-what-changes-and-what-to-watch","status":"publish","type":"post","link":"https:\/\/photonconsole.com\/blog\/migrating-from-amazon-ses-what-changes-and-what-to-watch\/","title":{"rendered":"Migrating from Amazon SES: What Changes and What to Watch"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Leaving SendGrid means getting your content out. Leaving SES is a different problem entirely, because SES holds almost nothing of yours.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">No templates, usually. No meaningful template engine. A light suppression list. What it holds instead is plumbing \u2014 IAM users, SNS topics, configuration sets, event destinations and IAM roles, scattered across one or more AWS regions and wired into each other.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You cannot export plumbing. You decommission it. That makes this migration less an extraction and more an untangling, and the risk is not losing data \u2014 it is leaving things switched on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quick Answer: What Actually Changes?<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>What you have on SES<\/th><th>What replaces it<\/th><th>Effort<\/th><\/tr><\/thead><tbody><tr><td>SMTP credentials derived from an IAM user<\/td><td>A provider-issued username and password<\/td><td>Minutes<\/td><\/tr><tr><td>Configuration set referenced in every send<\/td><td>Nothing \u2014 the reference disappears<\/td><td>Find every call site<\/td><\/tr><tr><td>Event destination to SNS, Firehose or EventBridge<\/td><td><strong>One webhook endpoint<\/strong><\/td><td>Rebuild the consumer<\/td><\/tr><tr><td>SNS topic plus its subscribers and IAM roles<\/td><td>Nothing \u2014 tear it down<\/td><td>Careful decommissioning<\/td><\/tr><tr><td>Verified identities, per region<\/td><td>One set of DNS records<\/td><td>Low<\/td><\/tr><tr><td>Account-level suppression list<\/td><td>Export and import it<\/td><td>Do before first send<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Note the third and fourth rows. Most of the engineering in this migration is replacing a multi-resource AWS event pipeline with a single HTTPS endpoint \u2014 which is simpler afterwards, and real work to get to.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The provider-agnostic sequencing \u2014 DNS TTL, SPF lookup limits, the ramp, the rollback plan \u2014 is in our guide to <a href=\"https:\/\/photonconsole.com\/blog\/migrate-email-provider-without-downtime\/\">migrating email providers without downtime<\/a>. Follow that ordering alongside this.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">First, Be Honest About Why You Are Leaving<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This matters because it determines whether the migration is a good idea, and the honest answer is uncomfortable for anyone selling an alternative.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>You will not save money per email.<\/strong> SES is the cheapest way to send email at volume, by a wide margin, and essentially every managed provider \u2014 including this one \u2014 costs more per message. If your reason for leaving is the invoice, stop and re-check, because the invoice is going up.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The defensible reasons to leave are operational:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>You are the deliverability team.<\/strong> SES gives you infrastructure at close to cost and leaves bounce handling, complaint processing, suppression hygiene and reputation monitoring to you. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/amazon-ses-sending-limits\/\">SES sending limits<\/a> covers the enforcement thresholds that come with that.<\/li>\n\n\n\n<li><strong>The notification pipeline costs more to run than it is worth<\/strong> at your volume \u2014 four AWS resources and an on-call rota to receive bounce events.<\/li>\n\n\n\n<li><strong>Nobody on the team wants to own it.<\/strong> A legitimate reason, and the most common real one.<\/li>\n\n\n\n<li><strong>You need delivery data your product team can query<\/strong> without building it on Firehose and a warehouse.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Those are good reasons. &#8220;It is expensive&#8221; is not one, and a migration undertaken for the wrong reason tends to get reversed in six months.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Deciding Whether This Migration Is Worth It<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Work out your monthly send cost on SES, then the same volume on the new provider \u2014 expect it to rise<\/li>\n\n\n\n<li>Estimate hours per month currently spent on bounce handling, suppression and reputation monitoring<\/li>\n\n\n\n<li>Multiply those hours by a loaded engineering rate; that is what the price difference is buying<\/li>\n\n\n\n<li>Add the cost of the AWS resources in the notification pipeline and whoever is on call for them<\/li>\n\n\n\n<li>If the time saved is worth more than the price increase, migrate. If not, stay \u2014 and fix the pipeline instead<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">What the Notification Pipeline Actually Is<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Bounce-Architecture_-SES-vs-Managed-Relay-1024x576.png\" alt=\"The Amazon SES notification pipeline shown as a chain of AWS resources from configuration set through event destination and SNS topic to a consumer, compared with a single webhook endpoint\" class=\"wp-image-552\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Bounce-Architecture_-SES-vs-Managed-Relay-1024x576.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Bounce-Architecture_-SES-vs-Managed-Relay-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Bounce-Architecture_-SES-vs-Managed-Relay-768x432.png 768w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Bounce-Architecture_-SES-vs-Managed-Relay-1536x864.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Bounce-Architecture_-SES-vs-Managed-Relay.png 1672w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Four or five AWS resources on one side. One HTTPS endpoint on the other.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the thing people underestimate. On most providers, &#8220;tell me about bounces&#8221; is a URL you paste into a settings field. On SES it is a graph.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A configuration set carries an <strong>event destination<\/strong>, and per the <a href=\"https:\/\/docs.aws.amazon.com\/cli\/latest\/reference\/sesv2\/create-configuration-set-event-destination.html\" target=\"_blank\" rel=\"noopener\">SES v2 CLI reference<\/a> that destination can be one of five types:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Destination type<\/th><th>What it also requires<\/th><\/tr><\/thead><tbody><tr><td><code>SnsDestination<\/code><\/td><td>A <code>TopicArn<\/code> \u2014 plus the topic&#8217;s own subscribers<\/td><\/tr><tr><td><code>KinesisFirehoseDestination<\/code><\/td><td>An <code>IamRoleArn<\/code> and a <code>DeliveryStreamArn<\/code><\/td><\/tr><tr><td><code>CloudWatchDestination<\/code><\/td><td><code>DimensionConfigurations<\/code><\/td><\/tr><tr><td><code>EventBridgeDestination<\/code><\/td><td>An <code>EventBusArn<\/code> \u2014 default bus only<\/td><\/tr><tr><td><code>PinpointDestination<\/code><\/td><td>An <code>ApplicationArn<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">And the events it can match are ten specific types: <code>SEND<\/code>, <code>REJECT<\/code>, <code>BOUNCE<\/code>, <code>COMPLAINT<\/code>, <code>DELIVERY<\/code>, <code>OPEN<\/code>, <code>CLICK<\/code>, <code>RENDERING_FAILURE<\/code>, <code>DELIVERY_DELAY<\/code> and <code>SUBSCRIPTION<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So a typical &#8220;we handle bounces&#8221; setup is a configuration set, an event destination, an SNS topic, an SQS queue or Lambda subscribed to that topic, and an IAM role permitting the whole chain. Five resources, each with its own permissions, each capable of failing independently.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Inventory what exists before you change anything\naws sesv2 list-configuration-sets --region us-east-1\n\naws sesv2 get-configuration-set-event-destinations \\\n  --configuration-set-name my-config-set --region us-east-1\n\naws sns list-topics --region us-east-1\naws sns list-subscriptions-by-topic --topic-arn &lt;arn&gt; --region us-east-1\n\n# Repeat for EVERY region you have identities in.\n# Verify these commands against the current CLI reference\n# before relying on them in a script.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Common Mistake<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Decommissioning in the wrong direction. The instinct is to delete the SNS topic first because it looks like the top of the chain. Do the opposite \u2014 leave every AWS resource running until your new webhook has been receiving events correctly for days. An SNS topic nobody reads costs almost nothing; a deleted topic while SES is still generating bounce events means those events are gone, and you will not know which addresses bounced during the gap. Tear down from the consumer inward, last of all the configuration set, and only after the old sending path is fully retired.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Your SMTP Credentials Are an IAM User<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the detail that creates an orphan after most SES migrations, and it is a security point rather than a tidiness one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Amazon&#8217;s <a href=\"https:\/\/docs.aws.amazon.com\/ses\/latest\/dg\/send-email-concepts-credentials.html\" target=\"_blank\" rel=\"noopener\">credentials documentation<\/a> is explicit: &#8220;Although your Amazon SES SMTP credentials are different than your AWS access keys and IAM user access keys, Amazon SES SMTP credentials are actually a type of IAM credentials.&#8221; Creating them requires permission for <code>iam:ListUsers<\/code>, <code>iam:CreateUser<\/code>, <code>iam:CreateAccessKey<\/code> and <code>iam:PutUserPolicy<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In other words, generating SMTP credentials created an IAM user with a send policy attached. When you migrate, that user remains \u2014 with live credentials and permission to send email from your domain, sitting in an account nobody is auditing for email any more.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quick Fix<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Finding and Retiring the SES IAM Users<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>List IAM users and look for ones with an SES send policy attached \u2014 these are your SMTP credentials<\/li>\n\n\n\n<li>Check each one&#8217;s last-used date before touching it; something may still be sending through it<\/li>\n\n\n\n<li>Deactivate the access key first rather than deleting the user \u2014 deactivation is reversible<\/li>\n\n\n\n<li>Wait out your full rollback window, then delete the key and the user<\/li>\n\n\n\n<li>Do this for every region and every environment, including the staging user someone made once<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Deactivate rather than delete while rollback is still possible. A deactivated key can be re-enabled in seconds; a deleted IAM user means recreating credentials and reconfiguring under pressure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Replacing the Pipeline With One Endpoint<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The replacement is genuinely simpler, and the work is in the translation rather than the plumbing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your new provider posts delivery events to an HTTPS endpoint you own. On PhotonConsole that is a single field in the dashboard \u2014 there is no topic, no subscription, no role. But the event vocabulary differs, so the consumer needs rewriting \u2014 and this is the moment to stop coupling your data model to a provider&#8217;s event names.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Map the provider's vocabulary to your own, once.\nconst EVENT_MAP = {\n  \/\/ SES                -&gt; yours\n  SEND:              'accepted',\n  DELIVERY:           'delivered',\n  BOUNCE:             'bounced',\n  COMPLAINT:          'complained',\n  REJECT:             'rejected',\n  DELIVERY_DELAY:     'deferred',\n  RENDERING_FAILURE:  'render_failed',\n  SUBSCRIPTION:       'preference_changed'\n};\n\n# Two SES-specific details that do not survive the move:\n#   - BOUNCE events carry a bounceType\/bounceSubType you may\n#     be branching on. Check what your new provider sends instead.\n#   - Events arrive wrapped in an SNS envelope, so your handler\n#     probably unwraps Message before parsing. That goes away.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That second note catches people. If your consumer parses an SNS notification envelope and then JSON-parses the <code>Message<\/code> field inside it, a direct webhook posting the event body at the top level will break it \u2014 and the failure looks like malformed JSON rather than a schema change. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-webhooks-explained-the-complete-engineering-guide-for-reliable-email-event-processing\/\">transactional email webhooks<\/a> covers verification and idempotency for the new consumer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Find Every Configuration Set Reference<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sends that use a configuration set name it explicitly, which means that string is scattered through your codebase and will become meaningless.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With the API it is a parameter. Over SMTP it is usually a header, which is easy to forget because it does not look like configuration.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Grep for all of these before you cut over\nX-SES-CONFIGURATION-SET\nConfigurationSetName\nX-SES-MESSAGE-TAGS\nconfiguration_set\n\n# The SMTP header form is the one people miss.\n# Leaving it in place is harmless on a new provider\n# but it is dead weight, and it misleads the next reader.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Message tags are worth noting too. If you used <code>X-SES-MESSAGE-TAGS<\/code> for reporting dimensions, whatever dashboards consume those tags need an equivalent on the new provider, or that reporting continuity breaks silently.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Everything Is Regional<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SES identities, configuration sets, suppression lists and sending quotas are all scoped per AWS region. A migration that audits only your primary region misses whatever exists elsewhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That matters in two directions. You may have verified identities in a region you forgot about, still able to send. And a multi-region setup means repeating the entire decommissioning sequence per region rather than once.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If your application also runs on EC2, note that the port 25 restriction is separate from any of this \u2014 our guide to <a href=\"https:\/\/photonconsole.com\/blog\/aws-ec2-port-25-blocked\/\">EC2 port 25 blocking<\/a> covers that, and it does not go away just because you changed email provider.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Good News: What SES Does Not Hold<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Short section, and genuinely reassuring after the above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SES has no template lock-in worth the name for most teams. If your email content is already rendered by your own code \u2014 which it almost certainly is, because SES gives you little reason to do otherwise \u2014 then the single hardest part of a SendGrid or Mailgun migration simply does not apply here.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Suppression is lighter too. SES maintains an account-level suppression list, and it is one list rather than six. Export it, import it before your first production send, and that part is done. It is still the only irreversible step you fully control, so do not skip it \u2014 mailing previously-bounced addresses from new infrastructure damages a reputation you have not built yet. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/how-to-reduce-email-bounce-rate-for-saas-applications-a-production-infrastructure-guide\/\">reducing bounce rate<\/a> covers why those first signals carry disproportionate weight.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Decommissioning Order<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Photon-Console-Teardown-Order-1024x576.png\" alt=\"Decommissioning order for an Amazon SES setup, tearing down from the event consumer inward to the configuration set and leaving DNS records until last\" class=\"wp-image-553\" style=\"aspect-ratio:1.7777777777777777;width:1200px;height:auto\" srcset=\"https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Photon-Console-Teardown-Order-1024x576.png 1024w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Photon-Console-Teardown-Order-300x169.png 300w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Photon-Console-Teardown-Order-768x432.png 768w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Photon-Console-Teardown-Order-1536x864.png 1536w, https:\/\/photonconsole.com\/blog\/wp-content\/uploads\/2026\/11\/Photon-Console-Teardown-Order.png 1672w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Build outward, tear down inward. The configuration set goes late and the MX record goes last.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Once traffic is fully moved and the rollback window has closed, work through the teardown in this order. Each step is safe only because the one before it is done.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Order<\/th><th>Tear down<\/th><th>Why this position<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>The consumer \u2014 Lambda, SQS subscriber or HTTPS endpoint<\/td><td>Nothing depends on it<\/td><\/tr><tr><td>2<\/td><td>The SNS topic and its subscriptions<\/td><td>Now has no readers<\/td><\/tr><tr><td>3<\/td><td>The event destination<\/td><td>Now points at nothing<\/td><\/tr><tr><td>4<\/td><td>The configuration set<\/td><td>No sends reference it any more<\/td><\/tr><tr><td>5<\/td><td>IAM access key \u2014 deactivate, then delete<\/td><td>Reversible until deleted<\/td><\/tr><tr><td>6<\/td><td>Verified identities, per region<\/td><td>Removes send capability entirely<\/td><\/tr><tr><td>7<\/td><td>DNS records, MAIL FROM MX last<\/td><td>Affects routing, not just auth<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Note<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the AWS account itself open, and keep SES in production access, for at least a full billing cycle after you reach 100% on the new provider. SES charges nothing for an account that is not sending, so the only cost is the resources you have deliberately left running \u2014 and getting back <em>into<\/em> production access is not instant if you ever surrender it. Our guide to SES sending limits covers the production access request; going through it a second time under pressure, because a migration needed reversing, is a situation worth spending nothing to avoid.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">DNS Cleanup, Last<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SES put records in your DNS and they should be the final thing you remove.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Easy DKIM CNAMEs.<\/strong> SES manages the keys behind CNAME records it gave you. Leave them while rollback is possible \u2014 DKIM is keyed by selector, so they cannot conflict with your new provider&#8217;s records.<\/li>\n\n\n\n<li><strong>The SPF include.<\/strong> One record, both providers inside it during the parallel run. Count your lookups before adding the new include: exceeding the limit in <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc7208.html\" target=\"_blank\" rel=\"noopener\">RFC 7208<\/a> returns <code>permerror<\/code> and fails authentication for both providers at once.<\/li>\n\n\n\n<li><strong>Custom MAIL FROM.<\/strong> If you configured one, there is an MX record pointing at SES. This is the one DNS change that needs real care, because an MX record affects mail routing rather than just authentication. Change it only after the old path is fully retired.<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code># During the parallel run \u2014 one SPF record, both senders\nv=spf1 include:amazonses.com include:relay.photonconsole.com ~all\n\n# New DKIM alongside the SES CNAMEs, different selector\nphoton._domainkey  CNAME  dkim.photonconsole.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Take the exact SES include from your own account rather than from this article. Our guide to <a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC<\/a> covers alignment, and the free <a href=\"https:\/\/www.photonconsole.com\/email-deliverability-checker.php\">email deliverability checker<\/a> shows what your domain publishes right now.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quotas and Cost, Compared Honestly<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>Amazon SES<\/th><th>Managed relay<\/th><\/tr><\/thead><tbody><tr><td>Price per email<\/td><td><strong>Lowest available<\/strong><\/td><td>Higher per message<\/td><\/tr><tr><td>Sending quota<\/td><td>Set per account and per region, not published<\/td><td>Published rate<\/td><\/tr><tr><td>Reputation enforcement<\/td><td>Thresholds with account pause<\/td><td>Provider manages the pool<\/td><\/tr><tr><td>Bounce and complaint handling<\/td><td>Yours to build and run<\/td><td>Included<\/td><\/tr><tr><td>Notification setup<\/td><td>Four to five AWS resources<\/td><td>One endpoint<\/td><\/tr><tr><td>Credentials<\/td><td>An IAM user with a send policy<\/td><td>A send-scoped key<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Read the first row and the last four together. That is the whole trade: you pay more per message and stop operating a pipeline. Whether that is worth it depends entirely on your volume and on what an engineer&#8217;s time is worth to you \u2014 which is a calculation only you can run, and worth running before you start rather than after.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A <a href=\"https:\/\/www.photonconsole.com\/relay.php\">reliable SMTP provider<\/a> with a flat per-thousand rate makes that arithmetic easy to do, because there is one number to multiply \u2014 PhotonConsole publishes a single rate rather than a tier table, so the comparison against your SES bill takes one calculation. <a href=\"https:\/\/www.photonconsole.com\/pricing.php\">Pricing<\/a> includes 5,000 free emails a month, which covers the entire parallel-running phase while SES is still live.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Validate, Then Cut Over<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Three confirmations before any production traffic moves.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Authentication aligns.<\/strong> Both providers in one SPF record, DKIM on separate selectors, no <code>permerror<\/code>. <a href=\"https:\/\/mxtoolbox.com\/\" target=\"_blank\" rel=\"noopener\">MXToolbox<\/a> confirms the combined record resolves, and <a href=\"https:\/\/www.mail-tester.com\/\" target=\"_blank\" rel=\"noopener\">Mail Tester<\/a> scores an end-to-end send. Receivers including <a href=\"https:\/\/support.google.com\/mail\/answer\/81126\" target=\"_blank\" rel=\"noopener\">Gmail<\/a> publish sender expectations worth reading before rather than after.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The new webhook receives a real bounce.<\/strong> Trigger one deliberately to a known-invalid address. Reading documentation is not verification.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Your suppression import is complete<\/strong>, checked by count rather than by assumption.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then ramp by percentage with SES still live, per the sequence in <a href=\"https:\/\/photonconsole.com\/blog\/migrate-email-provider-without-downtime\/\">the migration guide<\/a>. Keep the AWS resources running throughout, and tear them down in the order described above once you are done.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pro Tips<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Audit every region, not just the primary.<\/strong> Identities, config sets and quotas are all regional.<\/li>\n\n\n\n<li><strong>Deactivate IAM keys before deleting users.<\/strong> Deactivation is reversible; deletion under pressure is not.<\/li>\n\n\n\n<li><strong>Check whether your handler unwraps an SNS envelope.<\/strong> A direct webhook will break it, and the error looks like bad JSON.<\/li>\n\n\n\n<li><strong>Grep for the SMTP configuration-set header.<\/strong> It does not look like config, so it survives refactors.<\/li>\n\n\n\n<li><strong>Tear down from the consumer inward.<\/strong> The SNS topic goes late, not first.<\/li>\n\n\n\n<li><strong>Leave the custom MAIL FROM MX record until last.<\/strong> It affects routing, not just authentication.<\/li>\n\n\n\n<li><strong>Do the cost arithmetic before you start.<\/strong> Your per-email price is going up; make sure you know by how much.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Related Reading<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/photonconsole.com\/blog\/migrate-email-provider-without-downtime\/\">Migrating email providers without downtime<\/a> \u2014 the sequence and timing to follow alongside this<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/amazon-ses-sending-limits\/\">Amazon SES sending limits<\/a> \u2014 the enforcement thresholds that often start this decision<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/best-amazon-ses-alternatives-in-2026-an-infrastructure-level-comparison-for-engineering-teams\/\">Best Amazon SES alternatives<\/a> \u2014 if you have not chosen a destination yet<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/transactional-email-webhooks-explained-the-complete-engineering-guide-for-reliable-email-event-processing\/\">Transactional email webhooks explained<\/a> \u2014 rebuilding the event consumer<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/migrate-from-sendgrid\/\">Migrating away from SendGrid<\/a> \u2014 the contrasting case, where content is the lock-in<\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/aws-ec2-port-25-blocked\/\">AWS EC2 port 25 blocked<\/a> \u2014 a separate AWS constraint that survives the migration<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently Asked Questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Will I save money leaving Amazon SES?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. SES is the cheapest per-email option available and essentially every managed provider costs more per message. The reasons to leave are operational, not financial.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Are SES SMTP credentials the same as IAM keys?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not the same, but related. Amazon&#8217;s documentation states SMTP credentials &#8220;are actually a type of IAM credentials&#8221;, and creating them creates an IAM user with a send policy \u2014 which you must retire separately after migrating.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What SES event types do I need to remap?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ten: <code>SEND<\/code>, <code>REJECT<\/code>, <code>BOUNCE<\/code>, <code>COMPLAINT<\/code>, <code>DELIVERY<\/code>, <code>OPEN<\/code>, <code>CLICK<\/code>, <code>RENDERING_FAILURE<\/code>, <code>DELIVERY_DELAY<\/code> and <code>SUBSCRIPTION<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What should I delete first?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing. Leave every AWS resource running until the new webhook has worked for days and the old sending path is retired, then tear down from the consumer inward with the configuration set last.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why is my new webhook receiving malformed JSON?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Your consumer probably unwraps an SNS notification envelope and parses the <code>Message<\/code> field inside it. A direct webhook posts the event at the top level, so that unwrapping step has to go.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do I need to check every AWS region?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yes. Identities, configuration sets, suppression lists and quotas are all regional, so a single-region audit will miss whatever exists elsewhere.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does SES lock in my email templates?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rarely. Most SES users render content in their own code, which means the hardest part of a SendGrid or Mailgun migration does not apply here.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">When can I remove the SES DNS records?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After the rollback window closes. DKIM CNAMEs are safe to leave since selectors cannot collide; the custom MAIL FROM MX record should be the very last change.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SES migrations fail differently from other migrations. There is no content trapped in a vendor UI and no six-list suppression export. What there is instead is a dependency graph \u2014 a configuration set pointing at an event destination pointing at an SNS topic with its own subscribers and IAM roles, repeated per region, plus an IAM user holding live send credentials that nobody will think to retire.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So the discipline is reversed from an extraction. Change nothing early, add the new path alongside, prove the webhook works on real bounce events, and only then tear down \u2014 from the consumer inward, configuration set last, MAIL FROM record last of all. Deactivate IAM keys rather than deleting users while rollback is still a possibility.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And be clear-eyed about the trade. Your per-email cost is going up, and what you get for it is not sending capability \u2014 you already had that \u2014 but the end of operating a pipeline. A managed <a href=\"https:\/\/www.photonconsole.com\/\">email delivery service<\/a> replaces five AWS resources with one endpoint and a send-scoped credential, and absorbs the bounce handling, suppression and reputation work that SES correctly leaves to you. If that work is worth more to your team than the price difference, this is a good migration. If it is not, SES remains the right answer and there is no shame in staying.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Read More<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.photonconsole.com\/email-deliverability-checker.php\">Free Email Deliverability Checker<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.photonconsole.com\/amazon-ses-alternative.php\">PhotonConsole as an Amazon SES Alternative<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/migrate-email-provider-without-downtime\/\">Migrating Email Providers Without Downtime<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/spf-dkim-dmarc-explained-simply\/\">SPF, DKIM and DMARC Explained Simply<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/photonconsole.com\/blog\/smtp-configuration\/\">SMTP Configuration: Complete Setup Reference<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.photonconsole.com\/relay.php\">PhotonRelay: SMTP Relay Service<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.photonconsole.com\/pricing.php\">PhotonConsole Pricing<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Leaving SendGrid means getting your content out. Leaving SES is a different problem entirely, because SES holds almost nothing of yours. No templates, usually. No meaningful template engine. A light suppression list. What it holds instead is plumbing \u2014 IAM users, SNS topics, configuration sets, event destinations and IAM roles, scattered across one or more [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":551,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[590,589,587,588],"class_list":["post-550","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-email-deliverability","tag-and-the-honest-cost-trade","tag-not-content-iam-users","tag-ses-holds-plumbing","tag-sns-topics-and-configuration-sets-here-is-the-teardown-order"],"_links":{"self":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/550","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/comments?post=550"}],"version-history":[{"count":1,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/550\/revisions"}],"predecessor-version":[{"id":554,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/posts\/550\/revisions\/554"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media\/551"}],"wp:attachment":[{"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/media?parent=550"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/categories?post=550"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/photonconsole.com\/blog\/wp-json\/wp\/v2\/tags?post=550"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}