Choosing a SaaS hosting provider by who runs operations
Replay your last outage against each type of SaaS hosting provider and write down whose engineer does each step. One database restore, walked through a platform as a service such as Heroku or Render, through AWS or Azure with a consultant, and through a managed private cloud, shows which operating tasks each provider type takes on. It also shows the four tasks that stay with the product team under all three.
A deleted schedule at 3:55 on a Friday
Take a hypothetical software company, Brindlewood Dispatch, that sells scheduling and dispatch software to heating and plumbing contractors. Its developers run the hosting, and nobody on staff is a systems administrator. The application runs on EC2, and the database is MySQL on Amazon RDS, created in the console two years ago.
On Friday, September 18, 2026, at 3:55 p.m. Central, a bulk import for one contractor, account 4412, deletes 2,300 scheduled jobs. The contractor calls at 9:00 a.m. Monday because every technician's schedule is empty. By then 65 hours and 5 minutes have passed, and the first operating question is whether a restore point from before the import still exists.
Brindlewood has one because the RDS console defaults backup retention to seven days. An instance created through the API or the CLI with no retention value gets one day. Had a script built the same database, Monday's restore window would reach back only to about Sunday, and Friday afternoon would be gone. Render Postgres keeps 3 days on a Hobby workspace, which covers Monday's 65 hours with 6 hours and 55 minutes to spare and misses a Tuesday call at 89 hours. Upgrading during the incident does not help, because Render does not backfill the window after an upgrade. Pro workspaces and above get 7 days, the same window Azure SQL Database keeps by default (configurable from 1 to 35 days, or 1 to 7 on Basic).
Some instances turn up a retention of 0, which means automated backups are off. Turning them on causes an outage, as does turning them off, so the retention change goes into a maintenance window like any other.
Brindlewood's Monday breaks into these tasks:
- Find the import job and the minute it started, in the application logs.
- Pick the restore point: 20:54 UTC, one minute before the delete.
- Restore to a side copy.
- Give that copy network access and production's settings.
- Copy account 4412's rows back without duplicating jobs the contractor keyed in again on Monday.
- Ship a fixed import job.
- Call the provider when the team gets stuck.
Checking retention across a region
aws rds describe-db-instances \
--query 'DBInstances[*].[DBInstanceIdentifier,BackupRetentionPeriod,LatestRestorableTime]' \
--output tableA 1 usually means a script created the instance and never set the value, and a 0 means automated backups are off. The command reads the region the CLI is configured for, so repeat it with --region for each region you run in.
Defaults a point-in-time restore gives the new copy
An RDS point-in-time restore creates a new instance and leaves the source untouched. The new instance comes up in a system-selected Availability Zone with the default security group, the default subnet group and the default DB parameter group, unless the restore command names others. Left on the default security group, whose starting rule admits traffic only from members of that same group, the copy is usually out of reach of the application servers and the bastion host. The default parameter group runs MySQL in UTC, so a production group that set time_zone to US/Central does not carry over, and if that copy ever serves traffic, every NOW() written into a DATETIME column lands five hours off from the rows around it.
Swapping the Friday copy into service would throw away 65 hours of writes from every other contractor, so Brindlewood's fix is a side copy and a copy-back of one account's rows. Azure SQL Database offers the same two paths: database replacement, which renames the original and gives the restored copy its name, and data recovery, a script that moves rows from the restored copy into the original.
A swap earns its cost only when the whole database is lost. On RDS it takes two renames with modify-db-instance: the damaged instance first, since an existing name cannot be reused, then the restored copy under the production name. Each rename reboots the instance. The old DNS name disappears immediately and the new one takes about 10 minutes to resolve, and without --apply-immediately the rename waits for the maintenance window. A restored instance also keeps loading its data blocks from S3 in the background after it reports available, so it can run below full speed in production until that initialization finishes.
The copy-back holds the decisions a provider cannot make for the product team. INSERT IGNORE skips any job whose primary key already exists, which is the point, yet a job the contractor keyed in again on Monday under a new ID comes back as a second copy. Only the product team knows the natural key that catches it, such as the same customer, address and time slot. Foreign keys will not catch a missing parent row either. A default mysqldump file opens with SET FOREIGN_KEY_CHECKS=0, so MySQL loads a job note whether or not its job came back. Child tables get filtered on the job IDs that were restored, jobs load first, and an orphan count after the load shows whether the copy-back is clean. Running the load with mysql --show-warnings prints each skipped row's warning as the load runs.
Support plans stop short of this work. AWS Support excludes software debugging and system administration tasks. On a hypothetical $18,000 monthly AWS bill, Business Support+ costs 9 percent of the first $10,000 plus 7 percent of the next $8,000, or $1,460 a month. Enterprise Support's 10 percent would come to $1,800, so its $5,000 monthly minimum applies instead. Heroku puts general debugging of user applications out of scope. Costs beyond your cloud bill puts the support line into a full cost comparison, and Plan your next restore covers recovery targets and restore tests.
Every platform in the table below restores into a new database, and each copy arrives missing something production had.
| Platform | Where the restored copy lands | What the copy arrives without | Next step before users see data |
|---|---|---|---|
| Amazon RDS | A new DB instance, with the source untouched | Production's security group, subnet group and parameter group, unless the restore command names them | Name all three at restore time, then copy rows back, or rename both instances if the whole database is lost |
| Azure SQL Database | A new database on the same server, since an existing database cannot be overwritten | The original's tags, and restore points of its own until its first full backup and the log backup after it finish | For replacement, restore at the original service tier and compute size, then rename both databases with ALTER DATABASE. For one account's rows, write a data recovery script |
| Render Postgres | A new database instance | Connections: every service still points at the original | Point each service and tool at the recovery instance. The window is 3 days on Hobby, 7 on Pro or higher, and none on the Free compute plan |
| Heroku Postgres rollback | A new database with no connection to the primary, which takes time to become available | Primary status, until someone promotes it | Run heroku pg:promote, then remove the old add-on. The rollback period varies by plan, and heroku pg:info shows the earliest point |
Account 4412, restored on RDS
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier brindlewood-prod \
--target-db-instance-identifier brindlewood-prod-0918 \
--restore-time 2026-09-18T20:54:00Z \
--db-parameter-group-name brindlewood-mysql-params \
--db-subnet-group-name brindlewood-private \
--vpc-security-group-ids sg-0b7e4c1d2a3f59e60
mysqldump --single-transaction --no-create-info --insert-ignore \
--set-gtid-purged=OFF --where="account_id=4412" \
-h "$RESTORED_HOST" -u admin -p brindlewood jobs > account_4412_jobs.sql
mysql --show-warnings -h "$PROD_HOST" -u admin -p brindlewood < account_4412_jobs.sql
mysql -h "$PROD_HOST" -u admin -p brindlewood -e \
"SELECT COUNT(*) FROM job_notes n LEFT JOIN jobs j ON j.id = n.job_id WHERE j.id IS NULL"--restore-time takes UTC, so 3:54 p.m. Central on Friday becomes 20:54:00Z. The --where condition applies to every table named on the command line, so child tables keyed by job_id need their own dump, filtered on the restored job IDs and loaded after the jobs. --set-gtid-purged=OFF keeps a source with GTIDs enabled from writing a SET @@GLOBAL.gtid_purged statement into a file headed for production. The same setting keeps the file from turning off binary logging for the load, which would leave read replicas without the restored rows. The orphan query should return 0 before anyone tells the contractor the schedule is back. Delete brindlewood-prod-0918 once the contractor confirms the schedule, because it bills as a full instance until then.
Operating tasks by provider type: PaaS, hyperscaler, managed private cloud
On AWS or Azure, running the account well turns into a second job, and it lands on whoever knows the console best. For SaaS application hosting, the rows below are the tasks from the Brindlewood incident where the answer depends on the provider type. The consultant column carries two answers, AWS's default and the consultant's statement of work, and anything the statement of work leaves out goes back to the developers.
A PaaS takes operating system patching off the team completely. Microsoft's shared responsibility matrix moves the operating system to the provider under PaaS, while on EC2 the guest operating system, updates and security patches included, stays with the customer. For a small product that fits the platform, that gain is large. The cost shows up in the row about fitting the environment. To put a person's name against each row once a provider is chosen, work through Managed hosting: who does what?.
| Task | PaaS (Heroku, Render) | AWS or Azure plus a consultant | Managed private cloud (Petrichor) |
|---|---|---|---|
| Run the restore and put the copy on the network | A developer starts it from the dashboard or CLI. The platform owns the network, and each service gets repointed at the new database | A developer, or the consultant if the statement of work names restores, runs it and names the security group, subnet group and parameter group in the restore command | A Petrichor engineer, from the product team's ticket |
| Fit the environment to the application | The application adapts. Heroku cycles each dyno once every 24 hours plus up to 216 minutes, deletes local file changes on every restart, and its support will not rewrite code for compatibility | Whoever holds the account picks instance types, storage and database settings | The environment adapts. Petrichor engineers tune it until the application runs well |
| Reach someone for help | Heroku gives limited technical support on paid dynos or add-ons, with general debugging of user applications out of scope | AWS Support, priced by plan, excludes system administration and software debugging. The consultant answers as contracted | Names and phone numbers for people at Petrichor |
Four tasks stay with the product team whichever column it picks:
- Reading the application logs to find the import and the minute it started. AWS Support and Heroku both exclude debugging the customer's code.
- Choosing 20:54 UTC as the restore point, which only that log reading can justify.
- Deciding which rows go back and which of Monday's entries win. Microsoft's matrix keeps customer data, configurations and identities with the customer under IaaS, PaaS and SaaS alike.
- Shipping the fixed import job, under the process in Assign an owner to each release.
When the ticket goes to Petrichor
At Petrichor, Brindlewood's Monday starts when the product team files a ticket naming the database, account 4412, and the minute the import started. A Petrichor engineer does the database work and the restore.
The data decisions stay with the product team, which still chooses the rows that go back and ships the fixed import. We do not do web development or interface design. When a customer needs that work, we find someone who does.
The environment runs on our own hardware, with Linux and Windows servers, and Docker and Kubernetes for containerized applications. We built this cloud in the first place for software that was never designed to run in one. Managed hosting for SaaS and software vendors describes the service, and What our engineers handle lists the operating work.
Staying on AWS, Azure or a PaaS
Several kinds of software company should stay where they are, and each can check which kind it is before calling anyone.
A product built on managed services it cannot replace should keep them. Two commands list those services. Cost Explorer groups last month's bill by service, and a search of the codebase lists the SDK clients the code creates:
aws ce get-cost-and-usage \
--time-period Start=2026-08-01,End=2026-09-01 \
--granularity MONTHLY --metrics UnblendedCost \
--group-by Type=DIMENSION,Key=SERVICE
grep -rhoE "@aws-sdk/client-[a-z0-9-]+" --include=package.json --exclude-dir=node_modules . | sort -u
grep -rhoE "(client|resource)\(['\"][a-z0-9-]+" --include='*.py' . | sort -uThe Cost Explorer end date is exclusive, so that query returns August, and AWS charges $0.01 for each Cost Explorer API request. The second search catches boto3.client, boto3.resource and session clients in Python. Services the code reaches without an SDK, such as CloudFront or Route 53, appear only on the Cost Explorer list. Every service on either list needs a replacement the team would accept before anything leaves. Where nobody can name one, that piece of the product stays where it is.
Production that needs more than one facility or region belongs on a hyperscaler. Our cloud runs in one data center, and this whole comparison assumes production runs in one region.
Teams with a cloud operations group that already runs the account well gain little from a change, and the same goes for teams that want to run everything themselves. Managed hosting fits a team whose staff are all developers, where database work, backups and restores would otherwise land on whoever knows the console best.
For a small team whose application fits the platform, a PaaS is often the right SaaS hosting provider. Our engineers' rule of thumb is that a developer platform covers about 80 percent of what a production team needs. Business continuity, load balancing and a customer with an unusual requirement tend to sit in the remaining 20 percent.
Everyone else sits in the middle, where QA moves first and production stays put. Migration rehearsals cover what happens when production follows.
Book a call with a consultant and bring your last incident, written into the rows of the provider table.