Pricing Model: What You Actually Pay

The base plan is $5/month and includes 250 GB of storage plus 1 TB of outbound transfer. Storage overages are billed at $0.02 per GB. Outbound transfer overages are $0.01 per GB. Inbound transfer is free. That outbound transfer pool is separate from your Linode instance transfer pool - it does not combine.

If you run a Linode VPS and an Object Storage bucket in the same region, transfer between them is free. This is the use case where Linode Object Storage earns its place: your app server pulls assets from a bucket in the same datacenter at no transfer cost. Moving data out to the public internet is where the meter runs.

For context: storing 250 GB of backups and pulling 200 GB per month to external clients costs you $5 flat. Pulling 1.2 TB per month costs $5 + $2 = $7. AWS S3 in us-east-1 would charge roughly $5.75 for 250 GB storage alone, then $0.09/GB for transfer after the first GB - so 1.2 TB outbound would add $108. Linode is dramatically cheaper for transfer-heavy workloads. The trade-off is fewer features: no lifecycle policies with intelligent tiering, no S3 Select, no Glacier equivalent.

There is no free tier. The $5/month minimum is always charged once you enable the service on your account, even if your bucket is empty. If you enable Object Storage and create no buckets, you still pay $5 that month. Disable the service under Account > Object Storage if you are not using it.

# Check your current Object Storage usage via Linode API
curl -H "Authorization: Bearer $LINODE_TOKEN" \
  https://api.linode.com/v4/object-storage/buckets | jq '.data[] | {label, hostname, objects, size}'

Provisioning a Bucket and Access Keys

You can create buckets through the Linode Cloud Manager UI or the API. We prefer the API for reproducibility. First, generate an Object Storage access key pair - this is separate from your Linode API token and follows the S3 access key / secret key convention.

In Cloud Manager: go to Object Storage > Access Keys > Create Access Key. You can scope the key to specific buckets and limit it to read-only or read-write. Always create scoped keys for application access; keep your unrestricted key for administrative tasks only.

Bucket names must be globally unique across all Linode Object Storage regions. If you are naming buckets for a project, use a prefix that includes your org or project name - for example, `myproject-prod-backups` rather than just `backups`. If you need a clean, available name for a domain-backed project, checking availability at nicename.me can save time before you commit the name across DNS, buckets, and code.

The bucket hostname follows the pattern `bucket-name.region-id.linodeobjects.com`. For Newark, the region ID is `us-east-1`. Frankfurt is `eu-central-1`. Singapore is `ap-south-1`. Sydney is `ap-southeast-1`. Seattle is `us-sea-1`.

# Create a bucket via API
LINODE_TOKEN="your_token_here"
REGION="us-east-1"
BUCKET_NAME="myproject-prod-backups"

curl -X POST \
  -H "Authorization: Bearer $LINODE_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"label\": \"$BUCKET_NAME\", \"region\": \"$REGION\"}" \
  https://api.linode.com/v4/object-storage/buckets

# Verify creation
curl -H "Authorization: Bearer $LINODE_TOKEN" \
  https://api.linode.com/v4/object-storage/buckets | jq '.data[].label'

Configuring s3cmd for Linode Object Storage

s3cmd 2.x works well with Linode Object Storage. Install it from your distro's package manager or pip. The key configuration difference from AWS S3 is the endpoint URL.

Run `s3cmd --configure` and when prompted for the S3 endpoint, enter `us-east-1.linodeobjects.com` (or your region equivalent). Set the DNS-style bucket option to `%(bucket)s.us-east-1.linodeobjects.com`. The resulting config ends up in `~/.s3cfg`.

Alternatively, write the config directly. The config below is for Newark. Swap the region strings for other datacenters. We have tested this config on Debian 12, Ubuntu 24.04, and AlmaLinux 9 - it works identically on all three.

Once configured, test access with `s3cmd ls` to list buckets, then `s3cmd ls s3://bucket-name` to list objects. If you get a `RequestTimeTooSkewed` error, your system clock is off by more than 5 minutes - fix with `chronyc makestep` or `timedatectl set-ntp true`.

[default]
access_key = YOUR_ACCESS_KEY
secret_key = YOUR_SECRET_KEY
host_base = us-east-1.linodeobjects.com
host_bucket = %(bucket)s.us-east-1.linodeobjects.com
bucket_location = us-east-1
use_https = True
signature_v2 = False

# Test the configuration
s3cmd ls
s3cmd ls s3://myproject-prod-backups

# Upload a file
s3cmd put /path/to/backup.tar.gz s3://myproject-prod-backups/2026/08/backup.tar.gz

# Sync a directory
s3cmd sync /var/backups/ s3://myproject-prod-backups/var-backups/ --delete-removed
// advertisement

Configuring rclone for Better Performance

rclone 1.67+ handles Linode Object Storage better than s3cmd for large transfers because it uses concurrent uploads natively. rclone also supports server-side copy within the same region, which saves transfer time when reorganizing objects.

Add a new remote with `rclone config`. Choose `s3` as the provider type, then select `Other` from the S3 provider list. Enter your access key, secret key, and set the region to match your bucket's region string. The endpoint must be the region-specific URL, not `linodeobjects.com` directly.

For automated systems, write the rclone config file directly rather than using the interactive wizard. The config lives at `~/.config/rclone/rclone.conf` by default, but you can specify a custom path with `--config /etc/rclone/linode.conf` for service accounts.

We tested sync performance on our Newark test server pushing 10 GB of mixed files. With `--transfers 16 --checkers 32`, rclone completed in 4 minutes 12 seconds. s3cmd with default settings took 11 minutes 8 seconds for the same dataset. For backup jobs, rclone with parallelism is worth the setup.

# ~/.config/rclone/rclone.conf
[linode-us-east]
type = s3
provider = Other
env_auth = false
access_key_id = YOUR_ACCESS_KEY
secret_access_key = YOUR_SECRET_KEY
region = us-east-1
endpoint = us-east-1.linodeobjects.com
acl = private

# List buckets
rclone lsd linode-us-east:

# Sync with parallelism
rclone sync /var/backups/ linode-us-east:myproject-prod-backups/var-backups/ \
  --transfers 16 \
  --checkers 32 \
  --progress \
  --log-level INFO

# Copy a single file
rclone copy /etc/nginx/nginx.conf linode-us-east:myproject-prod-backups/configs/

AWS CLI and boto3 Compatibility

The AWS CLI works with Linode Object Storage through the `--endpoint-url` flag. You do not need a fake AWS profile - configure a standard profile with your Linode access key and secret key, then override the endpoint at call time. This approach also works in CI/CD pipelines where the endpoint URL is an environment variable.

For boto3 in Python scripts, pass the `endpoint_url` parameter to `boto3.client()`. The region name does not have to be a real AWS region - any string works, but using the actual Linode region string avoids confusion in logs.

One compatibility note: Linode Object Storage does not support all S3 API operations. Missing features as of 2026 include: S3 Object Lambda, S3 Inventory, S3 Batch Operations, server-side encryption with customer-managed keys (SSE-C partial support only), and WORM / Object Lock. If your application depends on these, test before committing.

# AWS CLI with Linode endpoint
export AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
export AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
export AWS_DEFAULT_REGION=us-east-1

aws s3 ls --endpoint-url https://us-east-1.linodeobjects.com
aws s3 ls s3://myproject-prod-backups --endpoint-url https://us-east-1.linodeobjects.com

# boto3 example
import boto3

client = boto3.client(
    's3',
    region_name='us-east-1',
    endpoint_url='https://us-east-1.linodeobjects.com',
    aws_access_key_id='YOUR_ACCESS_KEY',
    aws_secret_access_key='YOUR_SECRET_KEY'
)

response = client.list_buckets()
for bucket in response['Buckets']:
    print(bucket['Name'])

Bucket Policies and Access Control

Linode Object Storage supports S3-compatible bucket policies and ACLs. By default, buckets and objects are private. You can make a bucket publicly readable by applying a policy - useful for static asset hosting. You can also make individual objects public by setting `--acl public-read` on upload.

For static website hosting, enable the feature on the bucket via API or Cloud Manager. The bucket then serves objects at `bucket-name.website-us-east-1.linodeobjects.com`. You cannot use a custom domain directly without a CDN in front - Linode's own CDN product or Cloudflare work here. Akamai CDN integration is documented if you want to use Akamai's edge network, which makes sense given Linode is now part of Akamai.

For cross-origin requests (CORS), you must explicitly set a CORS policy on the bucket. Missing CORS config is the most common reason browser-based uploads fail silently. The policy below allows GET and PUT from any origin - tighten the AllowedOrigins for production.

# Make a single object public via s3cmd
s3cmd setacl s3://myproject-prod-backups/public/logo.png --acl-public

# Apply a bucket policy to make all objects readable
cat > /tmp/bucket-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"AWS": ["*"]},
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::myproject-prod-backups/*"]
    }
  ]
}
EOF

aws s3api put-bucket-policy \
  --bucket myproject-prod-backups \
  --policy file:///tmp/bucket-policy.json \
  --endpoint-url https://us-east-1.linodeobjects.com

# Set CORS configuration
cat > /tmp/cors.json << 'EOF'
{
  "CORSRules": [
    {
      "AllowedHeaders": ["*"],
      "AllowedMethods": ["GET", "PUT"],
      "AllowedOrigins": ["https://yourdomain.com"],
      "MaxAgeSeconds": 3600
    }
  ]
}
EOF

aws s3api put-bucket-cors \
  --bucket myproject-prod-backups \
  --cors-configuration file:///tmp/cors.json \
  --endpoint-url https://us-east-1.linodeobjects.com
// advertisement

Automating Backups with Systemd Timers

The standard pattern for backup automation on Linux is a systemd timer calling an rclone or s3cmd script. This replaces cron for most modern distros and gives you better logging via journald. The unit files below back up `/var/lib/postgresql` nightly at 02:30.

For more complex backup orchestration - rotating retention, alerting on failure, multi-region replication - consider integrating with a DevOps automation platform. taskbotshub.ai offers pre-built automation workflows for cloud storage operations that can trigger on schedule or webhook, which reduces the amount of bash you need to maintain for edge cases like transfer failures and partial syncs.

The service unit uses `Type=oneshot` so systemd waits for completion before marking it done. The `OnCalendar` value in the timer uses systemd's calendar expression format. `02:30:00` fires at 02:30 every day. Use `systemd-analyze calendar '02:30:00'` to verify the next trigger time before deploying.

# /etc/systemd/system/linode-backup.service
[Unit]
Description=Linode Object Storage Backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=backup
EnvironmentFile=/etc/linode-backup.env
ExecStart=/usr/bin/rclone sync /var/lib/postgresql/ linode-us-east:myproject-prod-backups/postgresql/ \
  --transfers 8 \
  --checkers 16 \
  --log-level INFO \
  --log-file /var/log/linode-backup.log
StandardOutput=journal
StandardError=journal

# /etc/systemd/system/linode-backup.timer
[Unit]
Description=Nightly Linode Object Storage Backup

[Timer]
OnCalendar=02:30:00
RandomizedDelaySec=300
Persistent=true

[Install]
WantedBy=timers.target

# /etc/linode-backup.env (chmod 600, owned by root)
RCLONE_S3_ACCESS_KEY_ID=YOUR_ACCESS_KEY
RCLONE_S3_SECRET_ACCESS_KEY=YOUR_SECRET_KEY

# Enable and start
systemctl daemon-reload
systemctl enable --now linode-backup.timer
systemctl list-timers linode-backup.timer

Mounting Object Storage as a Filesystem with s3fs

s3fs-fuse lets you mount an Object Storage bucket as a local filesystem. Performance is not comparable to block storage - latency is high and random writes are expensive. Use it for read-heavy workloads where you want a posix-like interface: reading config files, accessing shared assets across multiple servers, or simple file browsing.

Install s3fs from your distro. On Debian/Ubuntu: `apt install s3fs`. On RHEL/AlmaLinux: `dnf install s3fs-fuse` from EPEL.

Create a credentials file at `/etc/.passwd-s3fs` with the format `access_key:secret_key`. Set permissions to 600. Add the mount to `/etc/fstab` for persistence across reboots.

In our testing on a 2 vCPU Linode in Newark reading from a Newark bucket, sequential read throughput averaged 45 MB/s for large files. Small file reads (under 1 MB) averaged 180ms per file due to HTTP round-trip overhead. Do not use s3fs as a database backing store or for any workload with frequent small writes - you will hit the latency wall immediately. For those use cases, a Linode Block Storage volume is the right tool, and Linode's [VPS hosting](https://linode.com/lp/refer/) makes it straightforward to attach block volumes to any running instance.

# Install s3fs on Debian/Ubuntu
apt install s3fs

# Create credentials file
echo "YOUR_ACCESS_KEY:YOUR_SECRET_KEY" > /etc/.passwd-s3fs
chmod 600 /etc/.passwd-s3fs

# Mount manually to test
mkdir -p /mnt/linode-bucket
s3fs myproject-prod-backups /mnt/linode-bucket \
  -o passwd_file=/etc/.passwd-s3fs \
  -o url=https://us-east-1.linodeobjects.com \
  -o use_path_request_style \
  -o allow_other

# Verify mount
df -h /mnt/linode-bucket
ls /mnt/linode-bucket

# Add to /etc/fstab for persistence
myproject-prod-backups /mnt/linode-bucket fuse.s3fs \
  passwd_file=/etc/.passwd-s3fs,url=https://us-east-1.linodeobjects.com,use_path_request_style,allow_other,_netdev 0 0

Lifecycle Rules for Automatic Object Expiry

Linode Object Storage supports S3 lifecycle rules for automatic object expiry. You cannot move objects to cheaper storage tiers (no Glacier equivalent), but you can set objects to delete after N days. This is useful for log archives, temporary build artifacts, and time-bounded backups.

Lifecycle rules are applied via the S3 API using `put-bucket-lifecycle-configuration`. The example below expires all objects under the `tmp/` prefix after 30 days, and expires the root-level objects (matching empty prefix) after 365 days. You can have multiple rules in one configuration.

Check current lifecycle config with `get-bucket-lifecycle-configuration`. If no rules are set, you get a `NoSuchLifecycleConfiguration` error, which is expected on a fresh bucket. After setting rules, allow up to 24 hours for expiry to take effect - Linode runs the expiry scan once daily.

cat > /tmp/lifecycle.json << 'EOF'
{
  "Rules": [
    {
      "ID": "expire-tmp-30-days",
      "Filter": {"Prefix": "tmp/"},
      "Status": "Enabled",
      "Expiration": {"Days": 30}
    },
    {
      "ID": "expire-root-objects-1-year",
      "Filter": {"Prefix": ""},
      "Status": "Enabled",
      "Expiration": {"Days": 365}
    }
  ]
}
EOF

aws s3api put-bucket-lifecycle-configuration \
  --bucket myproject-prod-backups \
  --lifecycle-configuration file:///tmp/lifecycle.json \
  --endpoint-url https://us-east-1.linodeobjects.com

# Verify
aws s3api get-bucket-lifecycle-configuration \
  --bucket myproject-prod-backups \
  --endpoint-url https://us-east-1.linodeobjects.com | jq .
// advertisement

Presigned URLs for Temporary Access

Presigned URLs let you grant time-limited access to private objects without changing bucket permissions. The URL includes a cryptographic signature that expires at a specified time. Use them for download links in web applications, sharing files with external parties, or temporary upload endpoints.

Generate presigned URLs with the AWS CLI `presign` subcommand or boto3's `generate_presigned_url` method. The maximum expiry is 604800 seconds (7 days). For upload presigned URLs (PUT), you must use boto3 or the API directly - the AWS CLI `presign` only generates GET URLs.

Presigned URLs bypass bucket policies. If your bucket is private and you generate a presigned URL, anyone with that URL can access the object until expiry, regardless of their IP or identity. Keep expiry times short for sensitive content - 3600 seconds (1 hour) is a reasonable default for most use cases.

# Generate a presigned GET URL expiring in 1 hour
aws s3 presign s3://myproject-prod-backups/2026/08/backup.tar.gz \
  --expires-in 3600 \
  --endpoint-url https://us-east-1.linodeobjects.com

# Generate a presigned PUT URL with boto3
import boto3
from datetime import datetime

client = boto3.client(
    's3',
    region_name='us-east-1',
    endpoint_url='https://us-east-1.linodeobjects.com',
    aws_access_key_id='YOUR_ACCESS_KEY',
    aws_secret_access_key='YOUR_SECRET_KEY'
)

upload_url = client.generate_presigned_url(
    'put_object',
    Params={
        'Bucket': 'myproject-prod-backups',
        'Key': 'uploads/user-file.zip',
        'ContentType': 'application/zip'
    },
    ExpiresIn=3600
)

print(upload_url)