For a small SaaS with no regulator, I start at 30 days of searchable logs, 90 days in cheap archive, and 12 months for security and audit events. How long to keep application logs also depends on what you keep today: Supabase holds 1 day of logs on Free and 7 days on Pro, a window the plan chose.
How long to keep application logs: a starting window by log type, and what changes it
My default window for application logs at a small SaaS with no regulator is 30 days searchable and 90 days archived, with security and audit events at 12 months and payload logs at 30 days. Three things move the window: a contract or framework that names a number, how late questions arrive, and personal data in logs.
These numbers are where I start, not a standard. The retention section of error logging best practices says there is no universal retention period for a small app , and I agree with it: every window on this page is a starting point you adjust.
Retention is one control in logging and monitoring, the one that decides whether yesterday’s evidence still exists when someone asks for it.
The retention log starts here: one row per kind of log, with the window I’d start from and what moves it. The best practice for log retention, as far as one exists, is to write down how long logs should be retained for each type and why, then check that the setting matches what you wrote.
| Log type | Where I would start | What lengthens it | What shortens it |
|---|---|---|---|
| Request and application logs | 30 days searchable, 90 days archived | Customer disputes that arrive late; a contract term | Personal data in the lines |
| Error tracker events | The vendor plan’s window: by default, Sentry keeps errors 30 days on Developer and 90 days on Team and Business/Enterprise, and logs 30 days on each | A plan with a longer window | A plan with a shorter window; a plan change applies to new data only |
| Authentication and security events | 12 months, because questions about an intrusion can come weeks or months later | A framework that names a number (the checklist below) | Rarely worth it: cut fields instead |
| Audit logs of sensitive actions | 12 months, or the contract’s term | A customer contract that names a longer term | A contract term shorter than a year |
| Webhook and model request payload logs | 30 days | An open dispute about one payment or one answer | Customer content in the bodies |
| Metrics | 15 days at full resolution, longer when downsampled | Month-over-month comparisons | Disk size |
| Debug logs | 7 days, or off | An open investigation (a preservation hold) | No investigation open |
The error tracker row quotes Sentry’s data retention periods as read on 2026-10-03.
Three things change a window. A contract or framework that names a number comes first, and the checklist further down reads what the common ones actually say. Then the longest gap you have seen between an event and someone asking about it: NIST’s log management guide notes that data about a particular event could be needed weeks or months after it occurred. A billing dispute is one such late question; a post-incident review published to the status page is another (what is a status page, and when a small SaaS needs one covers that page). Personal data in the logs pulls the other way, toward a shorter window; the GDPR line is in the next section.
Setting and documenting log retention across the application’s services is deliverable 8.5 of the Production Hardening Sprint, because retention that is too short impairs investigation and uncontrolled retention increases cost and exposure.
What is a log retention policy
A log retention policy is one page: a register with a row for every place the app keeps logs, giving the window, where it is set, who can read it and how deletion happens, plus three rules on who may read logs, what must never be logged, and who reviews the register and when.
What log retention means is set out in NIST’s guide to computer security log management: log retention is “archiving logs on a regular basis as part of standard operational activities”, while log preservation is “keeping logs that normally would be discarded, because they contain records of activity of particular interest”, typically in support of incident handling or investigations. So I add one more line to a retention policy for logs, for a hold: who can stop deletion of a store while an investigation runs.
The register for one small SaaS, filled in, with ___ where your own values go:
| Store | What it holds | Window | Where it is set | Who can read it | How deletion happens | Last verified |
|---|---|---|---|---|---|---|
| App platform logs (Vercel) | Function output and request lines | 1 day on Pro | The Vercel plan; a drain for more | ___ | Vercel, at the plan’s window | ___ |
| Database and auth logs (Supabase) | API, Postgres and Auth logs | 7 days on Pro | The Supabase plan; a log drain for more | ___ | Supabase, at the plan’s window | ___ |
| Error tracker (Sentry) | Errors and logs | 30 or 90 days, by plan | The Sentry plan | ___ | Sentry, at the plan’s window | ___ |
| CDN or WAF logs | Edge requests and blocks | ___ | The provider’s dashboard | ___ | ___ | ___ |
| Email provider events | Deliveries and bounces | Theirs, window per their docs | The provider | ___ | The provider | ___ |
| Audit table | Sensitive actions | 12 months | A scheduled delete job | Admins only | The job; backups keep copies longer | ___ |
| VPS log files | /var/log/myapp/*.log | 30 days | /etc/logrotate.d/myapp | root and the app user | logrotate | ___ |
| Archive bucket (S3) | Logs shipped off the box and from drains | 90 days | A lifecycle rule | ___ | S3 expiration | ___ |
User data has its own schedule and triggers, in a data retention policy template for a small SaaS. That template has one row for security and application logs, which this register fills in, and it already puts webhook and LLM request logs at 30 days, the same window as the table above.
What goes wrong without it: logs deleted before the bug was investigated
Missing retention fails in four ways, and only one of them is about losing logs too soon.
| Situation | What happens | The register line that prevents it |
|---|---|---|
| Too short | A customer disputes something older than the plan keeps; on Supabase the window is set by plan, from 1 day on Free to 28 on Team | A window chosen on purpose, with a drain where the plan’s window is shorter |
| Unknown | A security questionnaire asks for the log retention period and nobody can answer for every store | One row per store, with its window and the date it was last checked |
| Uncontrolled | Request logs holding emails and IP addresses, kept for years, widen what any breach exposes | A window on every store, and a field list that keeps personal data out |
| The full disk | A server with no rotation keeps appending to /var/log until the disk fills | A logrotate file per app |
The full plan table and the log drain price are in Supabase’s plans and what each includes.
The uncontrolled case has a legal edge in the EU. Article 5 of the GDPR says personal data shall be “kept in a form which permits identification of data subjects for no longer than is necessary for the purposes for which the personal data are processed”. The article goes on to allow longer storage only where the data will be processed solely for archiving purposes in the public interest, scientific or historical research purposes or statistical purposes, under the safeguards of Article 89(1). As I read the article, a request log kept for debugging fits none of those. This is not legal advice. A full disk is the other failure I would expect, and rotation is what stops it; the logrotate section below shows the file.
Take a founder whose app runs on Supabase’s Pro plan with no log drain, so the only record of its API and auth requests is the project’s own logs. A customer writes about a failed sign-in from earlier than the plan’s 7-day log retention, and the request is no longer there to read. A log drain, which Supabase offers only on Pro, Team and Enterprise plans, would have sent a copy somewhere the founder controls. The plan chose the window, not the team.
The same gap shows up in my June and July 2026 audits, where 17 of the 21 third-party apps had no error tracking or alerting: when a user hits an error, nothing records it. I chose those 21, public and held-out third-party apps I audited, so the count describes them and is no rate for AI-built apps in general. In an app like that, the host’s default log window is usually the only record there is. Deciding what goes into each line, and what to redact before it is written, is part of how to do logging, a separate job from deciding how long to keep it.
How to set log retention periods, service by service
To set log retention periods, work through five places: the hosted services, any server you own, metrics and audit logs, the frameworks that name a number, and the cost, with sampling to cut it.
Hosted platforms and log services: where the window is set
Each hosted service sets its own window, and three of the five named services below set it by pricing plan. Every cell below comes from the vendor’s docs, read on 2026-10-03; where the docs I read say nothing, the cell says so.
| Service | What it keeps by default or by plan | Can you change it | How to keep longer | Source, checked |
|---|---|---|---|---|
| Supabase | 1 day on Free, 7 days on Pro, 28 days on Team | By plan: its logs page says retention is based on the project’s pricing plan | A log drain, on Pro, Team and Enterprise only | Supabase pricing and log drains docs, 2026-10-03 |
| Vercel runtime logs | 1 hour on Hobby, 1 day on Pro, 30 days on Pro with Observability Plus, 3 days on Enterprise, 30 days on Enterprise with Observability Plus | By plan, or with Observability Plus | Drains, on Pro and Enterprise | Vercel runtime logs and drains docs, 2026-10-03 |
| Amazon CloudWatch Logs | Indefinitely, by default | Yes, per log group, at any time | It already keeps everything until you set a window | CloudWatch Logs user guide, 2026-10-03 |
| Google Cloud Logging | 30 days in the _Default and user-defined buckets, if not customized | Yes, from 1 to 3,650 days; the _Required bucket’s period cannot be changed | A longer bucket retention; retention costs apply past the default | Cloud Logging buckets docs, 2026-10-03 |
| Sentry | By plan, as in the window table above | By plan: retention is set when data is ingested, based on the then-current plan; Sentry’s docs point to an upgrade or to support for enterprise retention options | A plan with a longer window, for errors | Sentry data retention periods, 2026-10-03 |
| Payment and email providers | Theirs, window per their docs | Theirs | Keep what you need in your own store | Each provider’s docs |
To check the log retention period on your own account, open each row’s console: the Supabase plan page, the Vercel project’s Logs tab, the Retention column on CloudWatch’s log groups, the bucket settings in Cloud Logging. The sources are Supabase’s log drains, Vercel’s runtime logs documentation, CloudWatch Logs retention settings and Cloud Logging buckets.
Vercel’s short windows matter most during an outage, and what to copy off the host while it is down belongs to what to do first when your app is down, so it is not repeated here. On CloudWatch, the opposite risk applies: by default log data is stored indefinitely, so a group nobody configured keeps growing. The rest of AWS logging and monitoring, beyond retention, is a separate setup.
To keep logs longer than a plan allows, I’d use a drain or export to object storage with a lifecycle rule that expires objects at the archive window. With S3 lifecycle rules, expiration actions define when objects expire, and Amazon S3 deletes expired objects on your behalf. Payment and email providers keep their own event logs on their own schedule, so the register lists them as “theirs, window per their docs” and nothing more.
Rotating logs on a box you own: Linux logrotate
Linux logrotate rotates, compresses and removes log files on a schedule, configured per app in a file under /etc/logrotate.d/. Old copies are rotated the rotate count of times before removal, so daily rotation with rotate 30 keeps about 30 days of old files. A dry run with logrotate -d prints debug messages and changes nothing.
What logrotate is used for, in the logrotate manual page: it “allows automatic rotation, compression, removal, and mailing of log files”, and each log file “may be handled daily, weekly, monthly, or when it grows too large”.
To configure logrotate for one app, leave the main file, /etc/logrotate.conf, alone and add a file in /etc/logrotate.d/. The logrotate project’s example config pulls that directory in with include /etc/logrotate.d, under the comment “packages drop log rotation information into this directory”; the manual itself documents the include directive, not the path.
The window arithmetic is mine, from the way the manual defines rotate (its row is in the options table below): the window is roughly the rotate count times the rotation period, so weekly with rotate 4 keeps about 28 days.
The logrotate options that decide a retention window, each “what it does” cell in the manual’s words, shortened:
| Option | What it does | When to use it |
|---|---|---|
daily, weekly | daily: log files are rotated every day; weekly: rotated on the given weekday (Sunday when none is given), or if the date is advanced by at least 7 days since the last rotation | daily for a busy app log, weekly for a quiet one |
rotate N | Log files are rotated count times before being removed; if count is 0, old versions are removed rather than rotated; default is 0 | Always: it sets the window |
maxage N | Remove rotated logs older than N days; the age is only checked if the log file is to be rotated | A hard age cap on top of the count |
size, maxsize | size: rotated only if bigger than size bytes, exclusive with the time options; maxsize: rotated when bigger than size bytes even before the time interval | maxsize on a log that can spike |
compress, delaycompress | Old versions are compressed with gzip by default; delaycompress postpones compression of the previous log file to the next rotation cycle | compress to save disk; delaycompress is in the FAQ |
missingok | If the log file is missing, go on to the next one without issuing an error message | An app that creates its log only when it first writes |
notifempty | Do not rotate the log if it is empty | A log that is quiet some days |
dateext | Archive old versions adding a date extension like YYYYMMDD instead of a number | Finding one day’s file quickly |
create, copytruncate | create: the log file is created again right after rotation; copytruncate: truncate the original in place after creating a copy, for a program that cannot be told to close its log file; a very small time slice between copying and truncating can lose some logging data | create when the app reopens its file on a signal, copytruncate when it cannot |
postrotate … endscript | The script is executed after the log file is rotated and before it is compressed | Telling the app to reopen its file |
A file for a Node.js or Python app that writes to /var/log/myapp/ and cannot be told to reopen its log, saved as /etc/logrotate.d/myapp:
/var/log/myapp/*.log {
daily
rotate 30
maxage 30
missingok
notifempty
compress
delaycompress
dateext
copytruncate
}
Before trusting it, run sudo logrotate -d /etc/logrotate.d/myapp: debug mode means no changes are made to the logs and the state file is not updated. The system journal keeps its own store with its own limits in journald.conf: SystemMaxUse= controls how much disk space the journal may use up at most, and MaxRetentionSec= sets the maximum time to store entries. The manual says time-based deletion should normally not be required, because size-based deletion with options such as SystemMaxUse= should be sufficient, and adds that to enforce data retention policies it might make sense to change MaxRetentionSec= from its default of 0, which turns the feature off. Container logs live in a different file with their own settings: how to clear Docker logs.
Rotation deletes, so an archive window longer than the rotate count only exists if the logs leave the box before rotation removes them; I ship them to object storage daily.
Retention beyond application logs: metrics and audit logs
Prometheus keeps local data for 15 days by default when neither retention flag is set, and --storage.tsdb.retention.time changes it. An audit log retention policy runs longer than the application window, with 12 months as my starting point or the contract’s term, because questions about who changed a role or exported data arrive months later.
How long Prometheus should retain data is set by two flags in Prometheus’s storage documentation. --storage.tsdb.retention.time sets how long to retain samples, --storage.tsdb.retention.size caps the bytes of storage blocks kept, and if neither is set the retention time defaults to 15d. When you use the size cap, the docs recommend setting it to at most 80-85% of the disk allocated to Prometheus. Prometheus’s local storage is limited to a single node’s scalability and durability, so Prometheus offers interfaces to remote storage systems; the docs add that, with proper architecture, years of data can be kept in local storage. What each metric type records is a separate subject: Prometheus metric types.
How long audit logs should be retained depends on who asks the questions, and they ask late: who changed this role, who exported that data. Where a customer’s contract names a term, that term wins; otherwise I aim for a year. Where the audit log is a table in the app’s own database, its window belongs in the user-data schedule as well, and backups hold copies past it; the backups section of the retention template covers that part. What the audit log should record in the first place is the subject of audit logging best practices for sensitive actions.
A log retention compliance checklist: what the common frameworks actually say
A log retention compliance checklist separates hard numbers from principles. The EU GDPR names no period: personal data stays identifiable no longer than its purposes need, which I take as a case for shorter log windows. HIPAA’s rule for covered entities and business associates keeps required documentation 6 years; whether that reaches audit logs is a question for counsel.
Each row with a source was read from the primary text on 2026-10-03, and each says whether the rule is required or good practice.
| Framework | Where it applies | What it says about log retention | Hard number | Required or good practice | Source |
|---|---|---|---|---|---|
| EU GDPR, Article 5(1)(e) | Personal data under the GDPR | Storage limitation: identifiable no longer than necessary for the purposes (quoted in the section above) | No number, a principle | Required where the GDPR applies | legislation.gov.uk, text as adopted |
| HIPAA, 45 CFR 164.316(b)(2)(i) | US covered entities and business associates | Retain the documentation required by paragraph (b)(1) for 6 years from its creation or the date it was last in effect, whichever is later; that section does not say whether audit logs count | 6 years, for that documentation | Required where HIPAA applies | GovInfo, 2024 CFR |
| NIST SP 800-92 (September 2006) | US federal guidance, not a legal requirement for a private SaaS | Policies should define how long each type of log data must or should be preserved | No number | Good practice | NIST CSRC |
| PCI DSS v4.0.1, Requirement 10.5.1 | Businesses whose self-assessment questionnaire includes it: SAQ D and SAQ A-EP print it, SAQ A does not | ”Retain audit log history for at least 12 months, with at least the most recent three months immediately available for analysis” | 12 months, 3 immediately available | Required where in scope | PCI council’s SAQ files |
| ISO/IEC 27001, SOC 2 | Companies seeking that certificate or report | No figure quoted here; I would let your own written policy set the number | Not quoted here | Your policy | Not quoted |
The texts behind the table are 45 CFR 164.316 and the PCI council’s self-assessment questionnaire D, in its v4.0 edition; v4.0 was retired on December 31, 2024, and v4.0.1 keeps the 10.5.1 text unchanged. NIST and the GDPR are linked above. My five-line checklist, in order:
- 01 List the frameworks that actually apply to the app, from contracts and questionnaires, not from a blog list.
- 02 Write the number each one requires next to it, or "no number" where it states a principle.
- 03 Set each log type's window to the longest that applies to it.
- 04 Keep personal data out of the logs, so a long window is safe to hold.
- 05 Keep the evidence of each configured window, with the checks in the next section.
Log sampling and the cost of keeping more
Log sampling keeps a fraction of repetitive events, such as the 1 in 10 health checks I’d start with, and keeps every event that matters: errors, authentication, security, audit and billing. Sampling by request id keeps a request’s lines together, and recording the rate on each event lets counts be scaled back.
Keeping logs costs money twice on a hosted log service: CloudWatch pricing lists a charge to collect (data ingestion) per GB and a charge to store (archival) per GB compressed. The table works through my assumption of one app writing 2 GB of logs a day, priced once in CloudWatch Logs and once as plain files in S3 Standard in the same region.
| Window | Logs held (uncompressed) | CloudWatch Logs Standard a month: collect + store | S3 Standard a month, uncompressed files |
|---|---|---|---|
| 30 days | 60 GB | $30.00 + $0.27 = $30.27 | $1.38 |
| 90 days | 180 GB | $30.00 + $0.81 = $30.81 | $4.14 |
| 365 days | 730 GB | $30.00 + $3.29 = $33.29 | $16.79 |
Prices checked 2026-10-03, US East (Ohio): CloudWatch Logs Standard at $0.50 per GB to collect and $0.03 per GB compressed to store, with the pricing page’s 0.15 compression ratio for each uncompressed byte; S3 pricing for S3 Standard at $0.023 per GB for the first 50 TB a month. The free tier, request charges and data transfer are left out.
On CloudWatch, collection is the bill: keeping a year instead of a month adds about $3 a month at this volume, while every GB you never send saves $0.50. That makes volume the lever, which is where sampling comes in.
For sampling, these are the steps I take, in order:
- Cut volume at the source first: raise the log level in production and drop noisy fields before you sample anything.
- Never sample errors, warnings, authentication and security events, audit events or billing events.
- Sample the repetitive and boring: health checks, successful static requests, debug lines, at a rate you write down; I’d start at 1 in 10.
- Sample by request id, not by line, so a kept request is kept whole.
- Record the sample rate on each kept event, so a count of 120 kept events at 1 in 10 reads as about 1,200.
How to verify it: verify the configured log retention window
A log retention window is verified in 6 checks: list every store, read each configured window, find each store’s oldest event and compare its age with the window, dry-run logrotate on servers, restore one archived day and search it, and send marked test values in the banned fields and confirm no store kept them.
- 01 List every log store the app writes to, one register row each. Pass: every store has a row, including the one nobody thought of. Evidence: the dated register.
- 02 Read each store's configured window from its console or CLI and record it. On CloudWatch,
aws logs describe-log-groupsreturnsretentionInDays, the number of days to retain each group's log events; AWS's CLI reference does not say how a group that never expires shows there, so check any group without a number in the console's Retention column, where Never Expire means no window. Pass: every row carries a window read today. Evidence: the CLI output or a screenshot per store. - 03 Check behavior, not only settings: search each store for its oldest event. Pass: on a store with steady traffic its age is about the window, neither younger (you keep less than you think, unless the store is newer than one window) nor far older (deletion is not happening). CloudWatch typically takes up to 72 hours after the retention setting to delete events, and in rare situations it might take longer, so an oldest event up to three days past the window is not yet a failure there. Evidence: the oldest timestamp per store.
- 04 On a server, run
logrotate -don the app's config, then list the rotated files with their dates. Pass: no more rotated files than the rotate count, and none older than about the window. Fewer files is normal on a box set up less than one window ago or wherenotifemptyskipped empty days, andmaxageis only checked when a rotation happens, so a file a little past the window after a quiet stretch is not yet a failure. Evidence: the listing. - 05 Where the register has an archive, a drain or export to object storage (Supabase and Vercel offer drains on Pro and above), fetch one archived day and search it for a request id your app logged in that same store on that day. Pass: found, within about an hour. Evidence: the query and its result.
- 06 On staging, send one request that carries a marked test value in each field the policy bans, such as a test email address and a fake token, then search every store for those exact values and for the request's own id. Pass: the request id is found and the marked values are not. Evidence: the request, the searches and their results.
The request id in the last check matters: a search for values nobody sent passes on any build, and a store that never logged the request passes too, so finding the id is what makes the empty result mean something. Log volume per day and the oldest event’s age per store are two numbers worth a dashboard tile, part of how to build an ops dashboard. Repeat the checks when a plan, a host or a log service changes.
On the sprint, deliverable 8.5 is verified this way: inspect the configured windows and verify retention behavior in supported systems. Deliverable 8.1 is verified by tracing a test request across services and checking log content for sensitive fields.
Where the sprint does this
The log retention work named in the first section, with its reason, is deliverable 8.5. Deliverable 12.5 in the published scope documents retention periods and handling rules for logs, user history, and abandoned records. The production readiness report, deliverable 13.1, is verified this way: account for all 123 IDs; keep failures visible until resolved and explain genuine non-applicable items. Legal advice and certification are separate services; the sprint implements and documents the technical data-handling controls. Hosting, paid tools and API usage remain in the client’s accounts.
Common questions about log retention
What is the 7 year retention policy?
The 7-year retention figure is usually US tax record keeping, not a rule for application logs: none of the frameworks quoted above sets 7 years for them. For tax records, the IRS says to keep records 3 years if none of its listed exceptions apply, and 7 years if you file a claim for a loss from worthless securities or bad debt deduction. HIPAA’s documentation rule, in the frameworks table, is 6 years.
Does logrotate run automatically?
Yes: normally logrotate is run as a daily cron job, or by logrotate.timer on systems that use systemd, and it only rotates the files its configs name. It will not modify a log more than once in one day unless that log rotates by size and logrotate runs more often, or unless the -f option is used.
How to run logrotate manually?
Run sudo logrotate -f /etc/logrotate.d/myapp to force a rotation now, even if logrotate doesn’t think one is necessary. Use -d instead to see what it would do in debug mode, where no changes are made to the logs. Run it as root or with sudo, as above.
What is the purpose of the delaycompress option in logrotate?
It postpones compression of the previous log file to the next rotation cycle, so the newest rotated file stays uncompressed for one more cycle. It only has effect together with compress, and the manual says it can be used when a program cannot be told to close its log file and might keep writing to the previous file for some time.
If you have a working app built with these tools and need it ready for real customers, this is what we do.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase