Bluesky avatar and banner: 1,000,000 bytes, PNG or JPEG

Bluesky's profile record caps avatar and banner at 1,000,000 bytes each, PNG or JPEG only. How to size a GPT Image 2.5 output and check the bytes in Pillow.

4 min readSume
All posts

Bluesky's profile record accepts PNG or JPEG for both the avatar and the banner, and each blob is capped at 1,000,000 bytes. That is a decimal million, so a file of exactly 1 MiB (1,048,576 bytes) is over the line. The lexicon sets no pixel size, so pick dimensions for how the picture looks and then check the byte count.

The limits, from the lexicon

The limits come straight from Bluesky's own lexicon file for app.bsky.actor.profile, read today. Nothing in that file mentions pixel dimensions or crop rules, so this post does not guess any.

Bluesky profile record fields (read 2026-10-04)
FieldTypeAcceptedLimit
displayNamestringtext64 graphemes (640 bytes)
descriptionstringtext256 graphemes (2,560 bytes)
avatarblobimage/png, image/jpegmaxSize 1000000
bannerblobimage/png, image/jpegmaxSize 1000000

Which sizes Sume can produce

Sume's image API lists ChatGPT Image 2.5 as openai/gpt-image-2.5. Its custom pixel sizes need both edges to be multiples of 16, a longest edge of at most 3840, an aspect ratio of at most 3:1, and 655,360 to 8,294,400 pixels in total.

For an avatar, 1024x1024 qualifies (1,048,576 pixels). For a wide banner, 1536x512 is exactly 3:1 and has 786,432 pixels, so it also qualifies. Ask for output_format jpeg, since the docs list png, jpeg, webp and svg as formats.

curl -X POST https://api.sume.com/v1/images \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-image-2.5",
    "prompt": "wide abstract banner, soft teal gradient, no text",
    "image_size": "1536x512",
    "output_format": "jpeg"
  }'

You cannot ask for a smaller file

The docs list output_compression (0 to 100 for webp and jpeg) in the schema but say no model advertises it yet, and sending it returns 400 unsupported_parameter. So you cannot ask Sume for a smaller file at request time. Size control happens after download.

  • Request the pixels you need, as jpeg.
  • If the response is 200, the image is in data[0].url. If it is 202, poll the job envelope as described in the docs and fetch the result.
  • Download the file and measure it before you upload anywhere.

Fit it under the cap

This loop re-saves a local file as JPEG at falling quality until it is at most 1,000,000 bytes. If it never gets there, the picture has too many pixels or too much fine detail for the cap, and you should shrink it.

import asyncio, io
from PIL import Image

LIMIT = 1_000_000  # decimal bytes, not 1 MiB

async def main():
    img = Image.open("avatar.png").convert("RGB")
    for quality in range(95, 40, -5):
        buf = io.BytesIO()
        img.save(buf, "JPEG", quality=quality, optimize=True)
        if buf.tell() <= LIMIT:
            with open("avatar.jpg", "wb") as f:
                f.write(buf.getvalue())
            print("quality", quality, "bytes", buf.tell())
            return
    raise SystemExit("still over 1,000,000 bytes: shrink the pixels")

asyncio.run(main())

Mistakes to avoid

  • Comparing against 1,048,576. The lexicon number is 1000000.
  • Uploading the PNG you just generated without measuring. Smooth gradients and noise both change PNG size a lot.
  • Sending output_compression to Sume. It returns 400 in v1.
  • Throwing away the original. Keep the generated file and the job record in case a platform asks where an image came from.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume